2727import math
2828import os
2929import sys
30+ import tempfile
3031import time
3132from datetime import datetime , timedelta , timezone
33+ from pathlib import Path
3234from typing import Any
3335from urllib .request import Request , urlopen
3436
5052NORAD_ID = os .environ .get ("NORAD_ID" , "25544" )
5153ASSET_NAME = os .environ .get ("ASSET_NAME" , "ISS (ZARYA)" )
5254CELESTRAK_URL = f"https://celestrak.org/NORAD/elements/gp.php?CATNR={ NORAD_ID } &FORMAT=JSON"
55+ TLE_CACHE_FILE = Path (os .environ .get (
56+ "ISS_TLE_CACHE_FILE" ,
57+ os .path .join (tempfile .gettempdir (), f"iss-{ NORAD_ID } -omm.json" ),
58+ ))
59+ FALLBACK_OMM = {
60+ "OBJECT_NAME" : "ISS (ZARYA)" ,
61+ "OBJECT_ID" : "1998-067A" ,
62+ "EPOCH" : "2026-05-25T10:07:58.537056" ,
63+ "MEAN_MOTION" : 15.49365963 ,
64+ "ECCENTRICITY" : 0.00074701 ,
65+ "INCLINATION" : 51.633 ,
66+ "RA_OF_ASC_NODE" : 52.7989 ,
67+ "ARG_OF_PERICENTER" : 96.3597 ,
68+ "MEAN_ANOMALY" : 263.8242 ,
69+ "NORAD_CAT_ID" : 25544 ,
70+ "BSTAR" : 0.00018076148 ,
71+ "MEAN_MOTION_DOT" : 9.654e-5 ,
72+ "MEAN_MOTION_DDOT" : 0 ,
73+ }
5374
5475
5576# ═══════════════════════════════════════════════════════════════════════════
6384_tle_refresh_interval : float = 3600.0
6485
6586
66- def fetch_tle_from_celestrak ( ) -> Satrec :
87+ def _satrec_from_omm ( omm : dict ) -> Satrec :
6788 global _cached_satrec , _tle_fetched_at , _tle_epoch_str , _tle_epoch_dt
6889
69- req = Request (CELESTRAK_URL , headers = {"Accept" : "application/json" })
70- with urlopen (req , timeout = 30 ) as resp :
71- data = json .loads (resp .read ().decode ())
72-
73- if isinstance (data , list ) and len (data ) > 0 :
74- omm = data [0 ]
75- else :
76- raise RuntimeError (f"Unexpected CelesTrak response: { str (data )[:200 ]} " )
77-
7890 sat = Satrec ()
7991 sat .sgp4init (
8092 WGS72 , 'i' ,
@@ -102,6 +114,51 @@ def fetch_tle_from_celestrak() -> Satrec:
102114 return sat
103115
104116
117+ def _normalize_omm_response (data : Any ) -> dict :
118+ if isinstance (data , list ) and len (data ) > 0 :
119+ return data [0 ]
120+ if isinstance (data , dict ):
121+ return data
122+ raise RuntimeError (f"Unexpected CelesTrak response: { str (data )[:200 ]} " )
123+
124+
125+ def _write_tle_cache (omm : dict ) -> None :
126+ try :
127+ TLE_CACHE_FILE .parent .mkdir (parents = True , exist_ok = True )
128+ TLE_CACHE_FILE .write_text (json .dumps (omm ), encoding = "utf-8" )
129+ except OSError as e :
130+ print (f" [WARN] Could not write TLE cache { TLE_CACHE_FILE } : { e } " )
131+
132+
133+ def load_cached_or_fallback_tle (reason : str ) -> Satrec :
134+ print (f" [WARN] TLE fetch failed ({ reason } ); using cached/fallback OMM" )
135+ if TLE_CACHE_FILE .exists ():
136+ try :
137+ omm = _normalize_omm_response (json .loads (TLE_CACHE_FILE .read_text (encoding = "utf-8" )))
138+ sat = _satrec_from_omm (omm )
139+ print (f" TLE cache epoch: { _tle_epoch_str } " )
140+ return sat
141+ except Exception as e :
142+ print (f" [WARN] Could not load TLE cache { TLE_CACHE_FILE } : { e } " )
143+
144+ sat = _satrec_from_omm (FALLBACK_OMM )
145+ print (f" TLE fallback epoch: { _tle_epoch_str } " )
146+ return sat
147+
148+
149+ def fetch_tle_from_celestrak () -> Satrec :
150+ global _cached_satrec , _tle_fetched_at , _tle_epoch_str , _tle_epoch_dt
151+
152+ req = Request (CELESTRAK_URL , headers = {"Accept" : "application/json" })
153+ with urlopen (req , timeout = 30 ) as resp :
154+ data = json .loads (resp .read ().decode ())
155+
156+ omm = _normalize_omm_response (data )
157+ sat = _satrec_from_omm (omm )
158+ _write_tle_cache (omm )
159+ return sat
160+
161+
105162def _epoch_to_jdsatepoch (epoch_str : str ) -> float :
106163 dt = datetime .strptime (epoch_str , "%Y-%m-%dT%H:%M:%S.%f" ).replace (tzinfo = timezone .utc )
107164 jd , fr = _datetime_to_jd (dt )
@@ -111,7 +168,14 @@ def _epoch_to_jdsatepoch(epoch_str: str) -> float:
111168def get_satrec () -> Satrec :
112169 global _cached_satrec , _tle_fetched_at
113170 if _cached_satrec is None or (time .time () - _tle_fetched_at ) > _tle_refresh_interval :
114- fetch_tle_from_celestrak ()
171+ try :
172+ fetch_tle_from_celestrak ()
173+ except Exception as e :
174+ if _cached_satrec is None :
175+ load_cached_or_fallback_tle (str (e ))
176+ else :
177+ _tle_fetched_at = time .time ()
178+ print (f" [WARN] TLE refresh failed ({ e } ); keeping epoch { _tle_epoch_str } " )
115179 return _cached_satrec
116180
117181
@@ -227,8 +291,7 @@ def on_startup(self, args):
227291 fetch_tle_from_celestrak ()
228292 print (f" TLE epoch: { _tle_epoch_str } " )
229293 except Exception as e :
230- print (f" FATAL: Could not fetch TLE: { e } " )
231- sys .exit (1 )
294+ load_cached_or_fallback_tle (str (e ))
232295
233296 def connect (self ):
234297 """Connect to server. Uses REST mode when OSH_BASE_URL is set, SDK otherwise."""
0 commit comments