Our vacation home is in northern Greece, and every summer the same question comes up: is that smoke on the horizon something I should worry about? I wanted Home Assistant to answer that — a map of active fires around the house, "nearest fire: X km" sensors, and a push notification if a fire gets within a threshold distance.
There's been an open feature request for NASA FIRMS data since 2021, and I initially assumed I'd have to write a converter script or a custom integration. Turns out: you don't need any custom code at all. Here's the trick.
The key discovery
NASA FIRMS (Fire Information for Resource Management System) provides near-real-time satellite fire detections (VIIRS, 375 m resolution). The documented API only offers CSV — which Home Assistant can't consume directly. But FIRMS also runs MapServer WFS endpoints, and those happily serve GeoJSON if you ask for it:
https://firms.modaps.eosdis.nasa.gov/mapserver/wfs/Europe/YOUR_MAP_KEY/?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAME=ms:fires_noaa20_24hrs&STARTINDEX=0&COUNT=1000&SRSNAME=urn:ogc:def:crs:EPSG::4326&BBOX=39.65,21.84,41.45,24.22,urn:ogc:def:crs:EPSG::4326&outputformat=geojson
That returns a plain GeoJSON FeatureCollection of fire hotspots — which is exactly what the core geo_json_events integration eats for breakfast. No custom component, no script, no proxy. Data updates every 15 minutes on NASA's side.
Step 1 — Get a free MAP_KEY
Register at API - Map Key - NASA | LANCE | FIRMS (free, just an email). Limit is 5,000 requests per 10 minutes — you'll use one every 5 minutes.
Step 2 — Build your URL
Adjust three things in the URL above:
- Region (
/wfs/Europe/): FIRMS regions includeEurope,USA_contiguous_and_Hawaii,Australia_NewZealand,SouthEast_Asia,South_America, etc. - Source (
TYPENAME=ms:fires_noaa20_24hrs): per satellite, 24 h or 7 days — e.g.fires_noaa20_24hrs,fires_noaa21_24hrs,fires_snpp_24hrs,fires_modis_24hrs. - BBOX (
BBOX=latS,lonW,latN,lonE): a bounding box around your location. For a radius r km aroundLAT/LON:latS = LAT - r/111,latN = LAT + r/111,lonW = LON - r/(111*cos(LAT)),lonE = LON + r/(111*cos(LAT)).
*The cosine takes the latitude in degrees — most tools (Excel, Python, many calculators) expect radians, so usecos(radians(LAT))there; if you get a negative value or a longitude offset of several degrees, radians bit you. Worked example, r = 100 km around 40.55/23.03:r/111 = 0.90→ latS 39.65 / latN 41.45;111 × cos(40.55°) = 84.3→100/84.3 = 1.19→ lonW 21.84 / lonE 24.22 — which is exactly the BBOX in the example URL above.
Step 3 — Add the GeoJSON integration
Settings → Devices & Services → Add Integration → GeoJSON → paste the URL, drag the location pin exactly onto your home (zoom in — an offset pin skews the built-in distance states), set the radius (e.g. 100 km).
You now get one geo_location.* entity per fire hotspot, automatically created and removed as detections come and go — and they show up on the standard map card out of the box:
type: mapgeo_location_sources: - geo_json_eventsentities: - zone.homedefault_zoom: 8theme_mode: auto
Optional: repeat Step 3 with fires_noaa21_24hrs and fires_snpp_24hrs for two more satellites — more overpasses per day, shorter detection gaps (at the cost of occasional duplicate markers for the same fire).
Step 4 — Sensors: hotspot count + nearest distance
Two template sensors (I created them as Template helpers via the UI — Settings → Devices & Services → Helpers — but YAML works the same). Count:
{{ states.geo_location | selectattr('attributes.source', 'eq', 'geo_json_events') | list | count }}
Nearest distance in km — computed from your zone's coordinates with distance(), so it stays correct even if the integration pin is slightly off:
{% set zlat = state_attr('zone.home','latitude') %}{% set zlon = state_attr('zone.home','longitude') %}{% set ns = namespace(best=none) %}{% for f in states.geo_location | selectattr('attributes.source','eq','geo_json_events') %} {% set d = distance(f.attributes.latitude, f.attributes.longitude, zlat, zlon) %} {% if d is not none and (ns.best is none or d < ns.best) %}{% set ns.best = d %}{% endif %}{% endfor %}{{ ns.best | round(1) if ns.best is not none else none }}
(Unit km, device class distance, state class measurement.)
Step 5 — Proximity alert
alias: "Wildfire proximity warning"description: "Push notification when a FIRMS hotspot comes within 15 km"triggers: - trigger: numeric_state entity_id: sensor.fire_nearest_distance below: 15actions: - action: notify.mobile_app_your_phone data: title: "🔥 Wildfire warning" message: >- Fire hotspot {{ states('sensor.fire_nearest_distance') }} km from home (NASA FIRMS satellite detection). data: url: /lovelace/fire # iOS: opens the map view on tapmode: single
The numeric_state trigger only re-fires after the value has gone back above the threshold — no notification spam while a fire stays close.
Step 6 (optional polish) — readable list + proper map icons
"Where is it?" list — the hotspots have no names, but a markdown card can compute distance, compass direction and the nearest town (small lookup table, no external geocoder). Swap the towns for your region:
{% set zlat = state_attr('zone.home','latitude') %}{% set zlon = state_attr('zone.home','longitude') %}{% set towns = [['Thessaloniki',40.6401,22.9444],['Katerini',40.2719,22.5025],['Serres',41.0856,23.5484],['Polygyros',40.3786,23.4442],['Veria',40.5238,22.2036],['Kilkis',40.9937,22.8753],['Edessa',40.8006,22.047]] %}{% set fires = states.geo_location | selectattr('attributes.source','eq','geo_json_events') | list %}{% if fires | count == 0 %}✅ No active fire hotspots within 100 km.{% else %}{% for f in fires %}{% set flat = f.attributes.latitude %}{% set flon = f.attributes.longitude %}{% set km = distance(flat, flon, zlat, zlon) %}{% set ns = namespace(town='?', td=9999) %}{% for t in towns %}{% set d = distance(flat, flon, t[1], t[2]) %}{% if d < ns.td %}{% set ns.td = d %}{% set ns.town = t[0] %}{% endif %}{% endfor %}{% set brg = (atan2((flon - zlon) * cos(flat * pi / 180), flat - zlat) * 180 / pi + 360) % 360 %}{% set dir = ['N','NE','E','SE','S','SW','W','NW'][(((brg + 22.5) / 45) | int) % 8] %}- 🔥 **{{ km | round(1) }} km {{ dir }}** · near **{{ ns.town }}** ({{ ns.td | round(0) | int }} km){% endfor %}{% endif %}
Map markers: the auto-generated entities render as a "GJE" initials bubble. Two lines of customize_glob plus a small SVG fix that (an emoji as friendly_name does NOT work — the marker initials logic mangles multi-byte characters into "?"):
homeassistant: customize_glob: "geo_location.geo_json_events_*": friendly_name: "Fire hotspot" entity_picture: /local/fire.svg
/config/www/fire.svg (MDI flame, orange on dark):
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <circle cx="12" cy="12" r="12" fill="#212121"/> <path fill="#ff7043" d="M17.66 11.2C17.43 10.9 17.15 10.64 16.89 10.38C16.22 9.78 15.46 9.35 14.82 8.72C13.33 7.26 13 4.85 13.95 3C13 3.23 12.17 3.75 11.46 4.32C8.87 6.4 7.85 10.07 9.07 13.22C9.11 13.32 9.15 13.42 9.15 13.55C9.15 13.77 9 13.97 8.8 14.05C8.57 14.15 8.33 14.09 8.14 13.93C8.08 13.88 8.04 13.83 8 13.76C6.87 12.33 6.69 10.28 7.45 8.64C5.78 10 4.87 12.3 5 14.47C5.06 14.97 5.12 15.47 5.29 15.97C5.43 16.57 5.7 17.17 6 17.7C7.08 19.43 8.95 20.67 10.96 20.92C13.1 21.19 15.39 20.8 17.03 19.32C18.86 17.66 19.5 15 18.56 12.72L18.43 12.46C18.22 12 17.66 11.2 17.66 11.2M14.5 17.5C14.22 17.74 13.76 18 13.4 18.1C12.28 18.5 11.16 17.94 10.5 17.28C11.69 17 12.4 16.12 12.61 15.23C12.78 14.43 12.46 13.77 12.33 13C12.21 12.26 12.23 11.63 12.5 10.94C12.69 11.32 12.89 11.7 13.13 12C13.9 13 15.11 13.44 15.37 14.8C15.41 14.94 15.43 15.08 15.43 15.23C15.46 16.05 15.1 16.95 14.5 17.5Z"/></svg>
Gotcha: customize is only applied when an entity is (re)created — after changing it, reload the core config and reload the GeoJSON integration (or wait for the next hotspot turnover).
Honest limitations
- This is not a real-time alarm. VIIRS satellites pass ~2× per day each (using 3 satellites shrinks the gap to a few hours), plus up to ~3 h processing latency. It reliably answers "where is it burning and how far from me" — a freshly ignited fire can be invisible for hours. Pair it with your country's official warning channel (in Europe: the
meteoalarmintegration has a forest-fire warning type). - A hotspot is the center of a 375 m satellite pixel — expect a few hundred meters of positional tolerance, and occasional non-wildfire detections (agricultural burns, industrial heat). The
confidenceproperty exists in the raw feed butgeo_json_eventsdoesn't filter on it. - All distances are great-circle (as the crow flies) — which is the metric you actually want for fires.
Dashboard tile:
pop-up:
Credits
Fire data: NASA FIRMS (https://firms.modaps.eosdis.nasa.gov/), free for this kind of use — don't share URLs containing your personal MAP_KEY.
Happy to answer questions — and if there's enough interest, I'm considering wrapping this into a proper HACS integration (config flow, confidence filtering, multi-satellite dedupe).
Disclaimer: This setup and write-up were created with the help of AI (Claude). Everything described is deployed and running live on my own instance (HA 2026.7) — the templates and configs above are copied from that working setup, not generated blindly.

