NotesWhat is notes.io?

Notes brand slogan

Notes - notes.io

from django.template.loader import render_to_string
from mail_auth_app import views as mail_views
import requests
import logging
from django.conf import settings
from django.utils import timezone
from datetime import timedelta
from django.core.cache import cache

from near_miss_app import constant_utils
from near_miss_app.geofence_detection_job import get_location_elevation
from mail_auth_app import views as mail_views
# from .flight_state_machine import FlightStateMachine
from django.utils import timezone
import uuid
import math
import pycountry
import reverse_geocoder as rg


logger = logging.getLogger(__name__)
near_miss_logger = logging.getLogger("near_miss_loggers")

def fetch_adsb_exchange_all_data():
cache_key = "adsb_all_data"

# FIX 1: do NOT break on empty list
cached_data = cache.get(cache_key)
if cached_data is not None:
return cached_data

url = constant_utils.ADSB_ALL_DATA_URL
api_key = settings.ADSB_API_KEY_TOKEN

headers = {
"api-auth": api_key,
"Accept": "application/json"
}

try:
response = requests.get(url, headers=headers, timeout=15)

# DEBUG (IMPORTANT)
near_miss_logger.info(f"ADSB STATUS: {response.status_code}")
near_miss_logger.info(f"ADSB RAW: {response.text[:200]}")

if response.status_code == 200:
json_data = response.json()

data = json_data.get("ac", [])

# IMPORTANT DEBUG
near_miss_logger.info(f"ADSB AIRCRAFT COUNT: {len(data)}")

cache.set(cache_key, data, timeout=10)
return data

near_miss_logger.error(f"ADSB API ERROR: {response.status_code}")
return []

except Exception as e:
near_miss_logger.exception(f"ADSB FETCH FAILED: {str(e)}")
return []


def get_elevation_cached(lat, lon):
key = f"elev:{round(lat, 2)}:{round(lon, 2)}"
val = cache.get(key)
if val is not None:
return val
val = get_location_elevation(lat, lon) or 0
cache.set(key, val, timeout=86400) # 1 day cache
return val


def get_country(lat, lon):
try:
result = rg.search((lat, lon), mode=1)
if result:
cc = result[0]["cc"]
country_name = next((c.name for c in pycountry.countries if c.alpha_2 == cc), None)
return {
"city": result[0].get("name"),
"state": result[0].get("admin1"),
"country": country_name
}
except Exception as e:
near_miss_logger.error(f"Country lookup failed: {str(e)}")
return {"city": None, "state": None, "country": None}


def get_nearest_airport(lat, lon, radius=3.5):
try:
response = requests.get(
"http://127.0.0.1:8000/api/nasr_data/fetch_nearby_airports/",
params={"lat": lat, "lon": lon, "dist": radius},
timeout=5
)
if response.status_code != 200:
near_miss_logger.error(f"Airport API failed: {response.text}")
return None
data = response.json()
airports = data.get("data", [])
if not airports:
return None
airports.sort(key=lambda x: x.get("distance", 999999))
nearest = airports[0]
address = get_country(nearest.get("latitude_deg"), nearest.get("longitude_deg"))
return {
"icao": nearest.get("icao_code") or nearest.get("ident"),
"iata": nearest.get("iata_code"),
"airport_name": nearest.get("name"),
"city": nearest.get("municipality") or address.get("city"),
"state": address.get("state"),
"country": address.get("country"),
"lat": nearest.get("latitude_deg"),
"lon": nearest.get("longitude_deg"),
"distance": nearest.get("distance")
}
except Exception as e:
near_miss_logger.error(f"Airport lookup failed: {str(e)}")
return None


# Email Notifications

# def send_flight_alert_email(
# tail_number,
# passenger_name,
# airport_data,
# recipient_email,
# event_type,
# tracking_url=None,
# planned_airport=None,
# fbo_name=None,
# fbo_address=None
# ):
# """Send email notifications for all flight events"""
# try:
# airport_code = airport_data.get("icao")
# city = airport_data.get("city")
# state = airport_data.get("state")
# country = airport_data.get("country")
# address = ", ".join([x for x in [city, state, country] if x])

# subject = ""
# body = ""

# if event_type == "GROUND MOVEMENT / TAXI":
# subject = f"Gate pushback Alert – {tail_number}"
# body = f"""
# Please be informed that Aircraft with Tail number {tail_number} carrying Mr. {passenger_name} has pushed off from the gate at airport {airport_code} ({address}) and is now preparing to take-off.

# To see the flight, please click here.

# {tracking_url or ""}

# You received this email because you asked us to let you know about this flight.
# Copyright 2026 • Sentient Jet. All rights reserved.
# """
# elif event_type == "TAKEOFF":
# subject = f"Take-Off Alert – {tail_number} from {airport_code}"
# body = f"""
# Please be informed that Aircraft with Tail number {tail_number} carrying Mr. {passenger_name} has taken off from airport {airport_code} ({address}) and is now Airborne.

# To see the flight, please click here.

# {tracking_url or ""}

# You received this email because you asked us to let you know about this flight.
# Copyright 2026 • Sentient Jet. All rights reserved.
# """
# elif event_type == "DESCENT_1000":
# subject = f"Preparing to Land - Alert – {tail_number} at {airport_code}"
# body = f"""
# Please be informed that Aircraft with Tail number {tail_number} carrying Mr. {passenger_name} is now preparing to land at airport {airport_code} ({address}). It has now reached an altitude at or below 1000 feet in its preparation to land.

# To see the flight, please click here.

# {tracking_url or ""}

# You received this email because you asked us to let you know about this flight.
# Copyright 2026 • Sentient Jet. All rights reserved.
# """
# elif event_type == "LANDING":
# subject = f"Landing Alert – {tail_number} at {airport_code}"
# body = f"""
# Please be informed that Aircraft with Tail number {tail_number} carrying Mr. {passenger_name} has landed at airport {airport_code} ({address}) and is now heading towards the Gate.

# To see the flight, please click here.

# {tracking_url or ""}

# You received this email because you asked us to let you know about this flight.
# Copyright 2026 • Sentient Jet. All rights reserved.
# """
# elif event_type == "PARKED":
# subject = f"Arrived at Gate Alert – {tail_number} at {airport_code}"
# body = f"""
# Please be informed that Aircraft with Tail number {tail_number} carrying Mr. {passenger_name} has arrived at the gate at airport {airport_code} ({address}).
# """
# if fbo_name:
# body += f"nThe gate is at {fbo_name}"
# if fbo_address:
# body += f" at {fbo_address}"
# body += f"""

# To see its location, please click here.

# {tracking_url or ""}

# You received this email because you asked us to let you know about this flight.
# Copyright 2026 • Sentient Jet. All rights reserved.
# """
# elif event_type == "DIVERSION" and planned_airport:
# planned_address = ", ".join([x for x in [planned_airport.get("city"), planned_airport.get("state"), planned_airport.get("country")] if x])
# subject = f"Diversion Alert – {tail_number}"
# body = f"""
# Please be informed that Aircraft with Tail number {tail_number} carrying Mr. {passenger_name} has to make a diversion stop. It is preparing to land at airport {airport_code} ({address}), instead of its planned destination of {planned_airport.get('icao')} ({planned_address}). We will be tracking the situation and will keep you posted.

# To see the flight, please click here.

# {tracking_url or ""}

# You received this email because you asked us to let you know about this flight.
# Copyright 2026 • Sentient Jet. All rights reserved.
# """
# else:
# return False

# from_email = mail_views.settings.EMAIL_HOST_USER
# return mail_views.send_email_graph(subject, body, [recipient_email], from_email, content_type="Text")

# except Exception as e:
# near_miss_logger.error(f"Flight alert email failed: {str(e)}")
# return False


from django.template.loader import render_to_string
from mail_auth_app import views as mail_views

# Mapping of event -> template file
EVENT_TEMPLATES = {
"GROUND MOVEMENT / TAXI": "emails/gate_pushback.html",
"TAKEOFF": "emails/takeoff.html",
"LANDING": "emails/landing.html",
"PARKED": "emails/parked.html",
"DESCENT_1000": "emails/descent_1000.html",
"DIVERSION": "emails/diversion.html",
}


def send_flight_alert_email_html(flight_data, event_type, recipient_email):
"""
flight_data: dict with keys:
tail_number, passenger_name, airport_code, city, state, country,
tracking_url, fbo_name, fbo_address, planned_airport_code, planned_city, planned_state, planned_country
event_type: string, one of ["TAKEOFF", "LANDING", "PARKED", "DESCENT_1000", "DIVERSION", "GROUND MOVEMENT / TAXI"]
recipient_email: str or list of str
"""

template_path = EVENT_TEMPLATES.get(event_type)
if not template_path:
# fallback
template_path = "emails/default.html"

# Render HTML
html_content = render_to_string(template_path, flight_data)

# Subject line based on event
if event_type == "GROUND MOVEMENT / TAXI":
subject = f"Gate Pushback Alert – {flight_data.get('tail_number')}"
elif event_type == "TAKEOFF":
subject = f"Take-Off Alert – {flight_data.get('tail_number')} from {flight_data.get('airport_code')}"
elif event_type == "LANDING":
subject = f"Landing Alert – {flight_data.get('tail_number')} at {flight_data.get('airport_code')}"
elif event_type == "PARKED":
subject = f"Arrived at Gate Alert – {flight_data.get('tail_number')} at {flight_data.get('airport_code')}"
elif event_type == "DESCENT_1000":
subject = f"Preparing to Land - Alert – {flight_data.get('tail_number')} at {flight_data.get('airport_code')}"
elif event_type == "DIVERSION":
subject = f"Diversion Alert – {flight_data.get('tail_number')}"
else:
subject = f"Flight Alert – {flight_data.get('tail_number')}"

# Standardize recipient list
if isinstance(recipient_email, str):
recipient_list = [recipient_email.strip()]
else:
recipient_list = recipient_email

# Send email via your existing Graph API function
return mail_views.send_email_graph(
subject,
html_content,
recipient_list,
mail_views.settings.EMAIL_HOST_USER,
content_type="HTML"
)
def process_flight_logic(state_obj, target_ac, planned_dest_iata=None):
"""
Flight event detection and email notifications using HTML templates.
Ensures correct sequence:
Gate Pushback → Takeoff → Descent 1000 → Landing → Parked → Diversion
Each event triggers exactly once per flight session using *_notified flags.
"""

if not target_ac:
return state_obj

now = timezone.now()
lat = target_ac.get("lat")
lon = target_ac.get("lon")
speed = target_ac.get("gs", 0.0) # Ground speed in knots
alt = target_ac.get("alt_baro") # Barometric altitude
v_rate = target_ac.get("baro_rate") or 0
prev_phase = state_obj.flight_phase
event = None
category = target_ac.get("category")

# Aircraft category thresholds
if category in ["A1", "A2"]:
takeoff_speed, landing_speed, taxi_max = 40, 60, 30
elif category in ["A3"]:
takeoff_speed, landing_speed, taxi_max = 60, 70, 35
else:
takeoff_speed, landing_speed, taxi_max = 80, 80, 40

# Determine altitude above sea level
adsb_ground = (alt == "ground")
alt_msl = 0 if adsb_ground else alt or 0
terrain = get_elevation_cached(lat, lon) if (alt_msl < 5000 and lat is not None and lon is not None) else 0
agl = max(0, alt_msl - terrain)

# Correct inconsistent ground altitude
if adsb_ground and speed > takeoff_speed and (v_rate > 200 or (state_obj.last_alt and state_obj.last_alt > 150)):
alt_msl = state_obj.last_alt or 300
agl = max(agl, 150)

# Initialize phase for new track
if not prev_phase:
if speed > takeoff_speed and (agl > 200 or v_rate > 100):
prev_phase = "AIRBORNE"
state_obj.flight_phase = "AIRBORNE"
state_obj.is_airborne = True
else:
prev_phase = "GROUND"
state_obj.flight_phase = "GROUND"
state_obj.is_airborne = False

# Reset flight session if idle on ground for > 30 min
if prev_phase == "GROUND" and state_obj.last_seen:
idle_time = (now - state_obj.last_seen).total_seconds()
if idle_time > 1800:
state_obj.flight_session_id = None

# Assign flight session ID on new takeoff and reset all notifications
if prev_phase == "GROUND" and agl > 150 and speed > takeoff_speed:
if not state_obj.flight_session_id:
state_obj.flight_session_id = uuid.uuid4().hex
state_obj.gate_pushback_notified = False
state_obj.takeoff_notified = False
state_obj.descent_1000_notified = False
state_obj.landing_notified = False
state_obj.parked_notified = False
state_obj.diversion_notified = False

new_phase = prev_phase

# EVENT DETECTION

# Only consider events if we have a valid flight session or new flight
if prev_phase == "GROUND":
# Gate Pushback / Taxi (new departure)
if 5 < speed < taxi_max and not state_obj.gate_pushback_notified and not state_obj.is_airborne:
event = "GROUND MOVEMENT / TAXI"
state_obj.gate_pushback_notified = True
# Start flight session
if not state_obj.flight_session_id:
state_obj.flight_session_id = uuid.uuid4().hex
# Reset other notifications for new flight
state_obj.takeoff_notified = False
state_obj.descent_1000_notified = False
state_obj.landing_notified = False
state_obj.parked_notified = False
state_obj.diversion_notified = False

# Takeoff
elif prev_phase == "GROUND" and agl >= 100 and v_rate > 300 and speed > takeoff_speed:
new_phase = "AIRBORNE"
if not state_obj.takeoff_notified:
event = "TAKEOFF"
state_obj.takeoff_notified = True

# Descent 1000
elif prev_phase == "AIRBORNE" and agl <= 1000 and v_rate < -300:
if not state_obj.descent_1000_notified:
new_phase = "DESCENT"
event = "DESCENT_1000"
state_obj.descent_1000_notified = True

# Landing
elif prev_phase in ["AIRBORNE", "DESCENT"] and agl <= 0 and speed < landing_speed:
new_phase = "GROUND"
if not state_obj.landing_notified:
event = "LANDING"
state_obj.landing_notified = True

# Parked at gate (after landing)
elif prev_phase == "GROUND" and speed <= 2 and state_obj.landing_notified:
if not state_obj.ground_stopped_at:
state_obj.ground_stopped_at = now
stop_duration = (now - state_obj.ground_stopped_at).total_seconds()
if stop_duration >= 180 and not state_obj.parked_notified:
event = "PARKED"
state_obj.parked_notified = True

# -------------------
# SEND EMAILS
# -------------------
if event:
airport_data = get_nearest_airport(lat, lon)
passenger_name = "Smith"
tracking_url = "https://airspacetracker.com"
recipient_email = "[email protected]"

if airport_data:
# Diversion handling
if event == "DESCENT_1000" and planned_dest_iata and airport_data.get("icao") != planned_dest_iata and not state_obj.diversion_notified:
planned_airport = {
"icao": planned_dest_iata,
"city": None,
"state": None,
"country": None
}
flight_data_diversion = {
"tail_number": state_obj.tail_number,
"passenger_name": passenger_name,
"airport_code": airport_data.get("icao"),
"city": airport_data.get("city"),
"state": airport_data.get("state"),
"country": airport_data.get("country"),
"tracking_url": tracking_url,
"planned_airport_code": planned_airport.get("icao"),
"planned_city": planned_airport.get("city"),
"planned_state": planned_airport.get("state"),
"planned_country": planned_airport.get("country")
}
send_flight_alert_email_html(flight_data_diversion, "DIVERSION", recipient_email)
state_obj.diversion_notified = True

# Build standard flight_data for HTML template
flight_data = {
"tail_number": state_obj.tail_number,
"passenger_name": passenger_name,
"airport_code": airport_data.get("icao"),
"city": airport_data.get("city"),
"state": airport_data.get("state"),
"country": airport_data.get("country"),
"tracking_url": tracking_url,
"fbo_name": "Signature Aviation",
"fbo_address": "Airport Terminal Road",
"planned_airport_code": planned_dest_iata or "",
"planned_city": None,
"planned_state": None,
"planned_country": None
}

send_flight_alert_email_html(flight_data, event, recipient_email)

state_obj.last_event_type = event
state_obj.last_event_time = now

# -------------------
# UPDATE FLIGHT STATE
# -------------------
state_obj.flight_phase = new_phase
state_obj.is_airborne = (new_phase != "GROUND")
state_obj.last_lat = lat
state_obj.last_lon = lon
state_obj.last_speed = speed
state_obj.last_alt = alt_msl
state_obj.last_vrate = v_rate
state_obj.last_seen = now
state_obj.last_known_lat = lat
state_obj.last_known_lon = lon
state_obj.save()

return state_obj





models.py


from django.db import models

class FlightAlertState(models.Model):
tail_number = models.CharField(max_length=50, unique=True, db_index=True)
# NEW → aviation state machine
flight_phase = models.CharField(max_length=30, db_index=True)
flight_session_id = models.CharField(max_length=100, null=True, blank=True, db_index=True)
last_event_time = models.DateTimeField(null=True, blank=True)
last_event_type = models.CharField(max_length=50, null=True, blank=True)
is_airborne = models.BooleanField(default=False, db_index=True)
is_device_active = models.BooleanField(default=True, db_index=True)
milestones_data = models.JSONField(default=dict, blank=True)
last_seen = models.DateTimeField(null=True, blank=True, db_index=True)
last_lat = models.FloatField(null=True, blank=True)
last_lon = models.FloatField(null=True, blank=True)
last_speed = models.FloatField(default=0.0)
last_alt = models.FloatField(null=True, blank=True)
last_vrate = models.FloatField(null=True, blank=True)
updated_at = models.DateTimeField(auto_now=True)
#######################################
descent_1000_notified = models.BooleanField(default=False)
landing_notified = models.BooleanField(default=False)
parked_notified = models.BooleanField(default=False)
diversion_notified = models.BooleanField(default=False)
takeoff_notified = models.BooleanField(default=False)
gate_pushback_notified = models.BooleanField(default=False)
departure_airport = models.CharField(max_length=20,null=True,blank=True)
destination_airport = models.CharField( max_length=20,null=True,blank=True)
ground_stopped_at = models.DateTimeField(null=True,blank=True)

last_known_lat = models.FloatField(null=True,blank=True)
last_known_lon = models.FloatField(null=True,blank=True)


class Meta:
indexes = [
models.Index(fields=["tail_number"]),
models.Index(fields=["last_seen"]),
]

def __str__(self):
return f"{self.tail_number} - {self.flight_phase}"
     
 
what is notes.io
 

Notes is a web-based application for online taking notes. You can take your notes and share with others people. If you like taking long notes, notes.io is designed for you. To date, over 8,000,000,000+ notes created and continuing...

With notes.io;

  • * You can take a note from anywhere and any device with internet connection.
  • * You can share the notes in social platforms (YouTube, Facebook, Twitter, instagram etc.).
  • * You can quickly share your contents without website, blog and e-mail.
  • * You don't need to create any Account to share a note. As you wish you can use quick, easy and best shortened notes with sms, websites, e-mail, or messaging services (WhatsApp, iMessage, Telegram, Signal).
  • * Notes.io has fabulous infrastructure design for a short link and allows you to share the note as an easy and understandable link.

Fast: Notes.io is built for speed and performance. You can take a notes quickly and browse your archive.

Easy: Notes.io doesn’t require installation. Just write and share note!

Short: Notes.io’s url just 8 character. You’ll get shorten link of your note when you want to share. (Ex: notes.io/q )

Free: Notes.io works for 14 years and has been free since the day it was started.


You immediately create your first note and start sharing with the ones you wish. If you want to contact us, you can use the following communication channels;


Email: [email protected]

Twitter: http://twitter.com/notesio

Instagram: http://instagram.com/notes.io

Facebook: http://facebook.com/notesio



Regards;
Notes.io Team

     
 
Shortened Note Link
 
 
Looding Image
 
     
 
Long File
 
 

For written notes was greater than 18KB Unable to shorten.

To be smaller than 18KB, please organize your notes, or sign in.