Automatically checks NASA satellite fire data every hour and sends a Telegram message when a new fire is detected in (or near) Bulgaria. Runs entirely on free GitHub Actions β no server, no computer left on, nothing to install on your phone except Telegram.
Currently configured for Bulgaria, but it works for any country or region by changing a couple of settings (see "Changing the monitored area" below).
- Every hour, pulls near-real-time fire detections from 4 NASA satellites (NOAA-20, NOAA-21, Terra, Aqua) covering Bulgaria.
- Keeps only detections inside Bulgaria's border, plus a 5 km safety buffer, so fires in Turkey / Romania / Greece are filtered out (border fires are kept).
- Groups detections within ~2 km into a single "fire" so one wildfire isn't reported as a dozen separate points.
- Measures every fire from a central point of your choosing (Sofia by default) and lists them nearest first, so the fire closest to you is the first thing you read. Anything within 20 km is escalated: the message leads with π¨ and the distance in capitals, so you can tell from the notification preview alone whether it concerns you.
- Remembers what it already told you, so you only get alerted about new fire activity β not the same fire every hour. A fire that burns for days is re-announced at most every 6 hours (2 hours if it is close to you), instead of on every satellite pass.
- With each alert it also sends a satellite photo of each fire (up to 10), with a red marker on the exact spot and a π Open map button underneath β so you can immediately see whether the fire is in forest, farmland, or a town.
- On request ("report" mode), sends a full snapshot of all fires in the last 24h.
- If NASA's servers are unreachable, it retries after 5 and 10 minutes, and only warns you (once) if everything is still down after ~15 minutes.
| File | Purpose |
|---|---|
fire_alerts_action.py |
The main program. Runs once per invocation. |
.github/workflows/fire-alerts.yml |
Tells GitHub to run the script hourly, and adds the manual "check/report" button. |
.github/workflows/find-hotspots.yml |
Runs find_hotspots.py on GitHub, monthly and on demand, so you can read the report from your phone. |
seen_fires.json |
Memory of already-reported detections. Starts empty ([]). GitHub updates it automatically. |
alert_cooldown.json |
When each fire was last announced, so long-burning fires aren't repeated hourly. Also automatic. |
excluded_zones.json |
Places to ignore β hot factories, flares, landfills. You edit this one by hand. |
geo.py |
Shared distance/direction helpers, plus the central point. Used by both scripts. |
find_hotspots.py |
Spots which locations keep repeating. Run it from the Actions tab or on your own machine. Suggests zones; changes nothing. |
AI_CONTEXT.md |
Technical explanation for an AI assistant if you want help modifying the project later. |
README.md |
This file. |
You need three free accounts/keys: a NASA key, a Telegram bot, and a GitHub repo. Everything below can be done from a phone browser.
- Go to https://firms.modaps.eosdis.nasa.gov/api/map_key
- Enter your email and request a key.
- NASA emails you a 32-character key (check spam). Keep it handy.
- In Telegram, open @BotFather.
- Send
/newbot, pick a name and a username ending inbot. - BotFather gives you a bot token like
123456789:AAH.... Copy it.
- Open your new bot in Telegram and send it any message (e.g. "hi").
- In a browser, open (replace
<TOKEN>):https://api.telegram.org/bot<TOKEN>/getUpdates - Find
"chat":{"id":NUMBERβ thatNUMBERis your chat ID.
- Create a new repository on https://github.com (private is fine).
- Add these files, keeping the same folder layout:
fire_alerts_action.py(repo root)seen_fires.json(repo root).github/workflows/fire-alerts.yml(create this exact path β typing the/characters makes the folders)find_hotspots.pyand.github/workflows/find-hotspots.yml(optional β the false-positive finder described further down)README.mdandAI_CONTEXT.mdare optional but recommended.
In the repo: Settings β Secrets and variables β Actions β New repository secret. Add these three (names must match exactly):
| Secret name | Value |
|---|---|
FIRMS_MAP_KEY |
your NASA key |
TELEGRAM_BOT_TOKEN |
your bot token |
TELEGRAM_CHAT_ID |
your chat ID |
(If the Settings tab is hidden on mobile, enable "Desktop site" in the browser menu. Settings only appears on repos you own.)
- Repo β Actions tab β Fire Alerts β Run workflow.
- Choose report from the dropdown and run it.
- Within a minute you should get a Telegram message β either a list of active fires or "no active fires detected ... β ".
That's it. From now on it runs by itself every hour.
- You do nothing. Hourly checks run automatically and message you only when there's new fire activity.
- To get a full status on demand (e.g. "is that fire still burning?"): Actions β Fire Alerts β Run workflow β choose report. This works from the GitHub mobile app too. report ignores the cooldown entirely β you asked, so it shows everything currently burning.
- Reading an alert: first a text message lists each fire β how far it is from your central point and in which direction ("34 km NE of Sofia"), its coordinates, how many satellite detections it has, how much power it is radiating in megawatts (the real measure of size), and when it was last seen in Bulgarian local time with UTC in brackets, plus a Google Maps link. Fires are listed nearest first, and the message's opening line names the closest one, so a phone notification preview already tells you whether anything is near you. Then, for the nearest fires, a satellite photo follows with a red marker on the fire and a π Open map button. (Tapping the photo just zooms it; use the button to open the map.) The satellite imagery is archival β it shows what the terrain normally looks like, not the fire or smoke itself.
Open .github/workflows/fire-alerts.yml and edit the env: block:
- Different country: change
FIRE_COUNTRY: "BGR"to another 3-letter code (e.g.GRCfor Greece). Then either remove theFIRE_BBOXline (uses NASA's country endpoint, currently unreliable) or set a new bounding box. - Different rectangle: set
FIRE_BBOXto"west,south,east,north"in degrees.
The precise border filtering is separate β it lives in fire_alerts_action.py as
BG_POLYGON. If you switch countries and want border-accurate filtering, either
replace that polygon with the new country's outline or set FILTER_TO_POLYGON = False to rely on the rectangle alone. See AI_CONTEXT.md for details.
Every fire is reported as a distance and direction from one reference point β by default the centre of Sofia. Fires are always listed from nearest to farthest, and the satellite photos follow the same order.
To move it, open .github/workflows/fire-alerts.yml and edit these three lines
in the env: block:
FIRE_CENTER_NAME: "Sofia"
FIRE_CENTER_LAT: "42.6977"
FIRE_CENTER_LON: "23.3219"FIRE_CENTER_NAMEβ whatever you want the alerts to call it ("home", "the village", "Plovdiv"). It only affects the wording.FIRE_CENTER_LAT/FIRE_CENTER_LONβ decimal degrees. The easiest way to get them: long-press the spot in Google Maps, and copy the two numbers it shows.
Commit and push; the next hourly run uses the new point. A typo can't break the alerts β an unreadable value is reported in the run log and the previous default is used instead.
Distances are straight-line ("as the crow flies"), not driving distance, and the direction is a compass bearing from your point to the fire.
Some places show up as "fires" every single day without ever burning β cement works, steel mills, refinery flares, power stations, big landfills. Satellites see hot, they report hot.
To silence one, add it to excluded_zones.json:
[
{"name": "Devnya cement works", "lat": 43.2237, "lon": 27.5619},
{"name": "Bobov dol power station", "lat": 42.2986, "lon": 22.9944, "radius_km": 1.5}
]lat/lonβ copy them straight out of the alert message or the Google Maps link.nameβ for your own benefit; it appears in the run log, never in Telegram.radius_kmβ optional, defaults to0.2(about 200 m).max_frpβ optional. A safety valve: a detection radiating more megawatts than this is reported anyway. Leave it out and the zone stays absolutely silent, as before.
Detections inside the ring are dropped silently: no message, no photo, no mention in the report. The workflow log records how many were suppressed and where, so you can check it is behaving.
Two things worth knowing:
- If the same factory keeps leaking through, widen just that one with
radius_km. The satellites place a hotspot slightly differently on each pass (MODIS pixels are about a kilometre across), so 200 m does not always cover a large industrial site. - A real fire inside the ring is also silenced β unless you give the zone a
max_frp. Keep the rings small so a wildfire next door still reaches you.
If you want a factory silenced on ordinary days but not when it is genuinely
ablaze, give it a max_frp:
{"name": "Devnya cement works", "lat": 43.2237, "lon": 27.5619, "radius_km": 0.5, "max_frp": 150}Watch the run log for a few weeks first to see what that site normally radiates, then set the ceiling comfortably above it. The log line reads "Reported despite an excluded zone, over its max_frp".
Changes take effect on the next hourly run β commit and push, nothing else.
You do not have to notice repeat offenders yourself. Every alert is logged in
seen_fires.json, so the history can be searched for spots that keep coming
back. find_hotspots.py does that: it groups nearby detections and reports any
location seen on several separate days, with its distance from your central
point, a Google Maps link and a ready-made line to paste into
excluded_zones.json. Locations already covered by a zone are
marked as such. It only reads β it never edits anything.
A real wildfire appears for a day or two and is gone. Anything showing up on most days of the month is a machine, not a fire.
From your phone (no setup): Actions β Find Hotspots β Run workflow. When the run finishes, open it and the report is printed on the run's summary page β no scrolling through logs. It also runs by itself on the 1st of each month, so there is a fresh report waiting whenever you think to look.
The Run workflow dropdown has three optional boxes:
| Box | Default | What it does |
|---|---|---|
| Separate days a spot must recur | 4 |
Lower it to 3 or 2 to catch weaker repeat offenders. |
| How close counts as the same source | 0.5 |
Kilometres. Raise it for a sprawling industrial site. |
| List every location | off | Tick it to see one-day fires too β a long report. |
The workflow needs no secrets and commits nothing.
From a computer, the same thing without GitHub:
python find_hotspots.py # normal use
python find_hotspots.py --min-days 3 # catch weaker repeat offenders
python find_hotspots.py --all # show every location, one-offs too
Worth checking every month or two. Once a zone is added its detections stop being recorded, so that spot gradually drops out of the report.
| Setting | Default | Meaning |
|---|---|---|
CLUSTER_DEG |
0.02 |
How close (in degrees, ~2 km) detections merge into one fire. |
NEAR_KM |
20.0 |
Fires this close to the central point are escalated with π¨. |
COOLDOWN_HOURS |
6.0 |
Stay quiet this long about a fire already announced. |
NEAR_COOLDOWN_HOURS |
2.0 |
Shorter window for fires within NEAR_KM. |
COOLDOWN_MATCH_KM |
3.0 |
How close a fire must be to a remembered one to count as the same fire. |
BUFFER_KM |
5.0 |
How far outside the border a fire is still reported. |
MAX_ITEMS |
35 |
Max fires listed per Telegram message. |
MAX_MAP_PINS |
10 |
Max satellite photos sent per alert (nearest fires first). |
MAP_HALF_SPAN_DEG |
0.02 |
Zoom of the satellite photo (~Β±2 km around the fire). Smaller = closer. |
RETRY_DELAYS |
[300, 600] |
Seconds to wait between retry attempts on NASA outages. |
EXCLUDE_RADIUS_KM |
0.2 |
Default radius (~200 m) around an entry in excluded_zones.json. |
FILTER_TO_POLYGON |
True |
Whether to apply the border-shape filter at all. |
CENTER_LAT / CENTER_LON |
Sofia | Point that distances are measured from (via FIRE_CENTER_LAT / FIRE_CENTER_LON). |
CENTER_NAME |
Sofia |
Name used in the messages (via FIRE_CENTER_NAME). |
LOCAL_TZ_NAME |
Europe/Sofia |
Timezone alert times are shown in, UTC always in brackets (via FIRE_TIMEZONE). |
MIN_CONFIDENCE |
nominal |
Minimum detection confidence (via FIRE_MIN_CONFIDENCE env). Accepts low/nominal/high or a 0β100 number; both scales are honoured. |
- Not for life-safety decisions. Satellites pass over only ~4β6 times a day, and detections arrive 1β3 hours after the pass. A new fire may take hours to appear. Always defer to official emergency services.
- Clouds and smoke can hide fires from satellites; absence from a report is strong but not absolute proof a fire is out.
- Scheduled runs use UTC and can be delayed 5β15 minutes when GitHub is busy.
- Inactive repos: GitHub pauses scheduled workflows after 60 days without repo activity. The hourly state-file commits normally keep it active; if it ever pauses, re-enable it with one tap in the Actions tab.
- Reignition: if a fire goes out and restarts later, the new detections have new timestamps, so you'll be alerted again.
- Distances are approximate. They are measured to the centre of a cluster of satellite detections, and a satellite places a hotspot to within roughly a kilometre. Treat "3 km away" as "a few kilometres away".
Fire data Β© NASA FIRMS (Fire Information for Resource Management System), near-real-time MODIS and VIIRS active fire products. Free for public use.
MIT β see LICENSE. Applies to this project's code only; NASA FIRMS data carries its own terms.