@@ -195,6 +195,24 @@ class ISSPublisher(PublisherBase):
195195 "urn:os4csapi:system:iss-position-publisher:v1" )
196196 ds_name = os .environ .get ("POS_DS_NAME" , "ISS Position (SGP4)" )
197197
198+ def __init__ (self ):
199+ # REST-only mode: bypass OSHConnect SDK when OSH_BASE_URL is set
200+ self ._rest_mode = bool (os .environ .get ("OSH_BASE_URL" ))
201+ if self ._rest_mode :
202+ import base64
203+ self .osh_address = os .environ .get ("OSH_ADDRESS" , "" )
204+ self .osh_user = os .environ .get ("OSH_USER" , "" )
205+ self .osh_pass = os .environ .get ("OSH_PASS" , "" )
206+ self ._base_url = os .environ ["OSH_BASE_URL" ]
207+ self ._is_go_server = "csapi-go" in self ._base_url
208+ self ._auth = "Basic " + base64 .b64encode (
209+ f"{ self .osh_user } :{ self .osh_pass } " .encode ()).decode ()
210+ self ._ds_id : str | None = None
211+ self .stats = {"published" : 0 , "errors" : 0 , "reconnects" : 0 }
212+ else :
213+ super ().__init__ ()
214+ self ._is_go_server = False
215+
198216 def configure_cli (self , parser : argparse .ArgumentParser ):
199217 parser .add_argument ("--tle-refresh" , type = float , default = 3600.0 ,
200218 help = "Seconds between TLE refreshes (default: 3600)" )
@@ -212,6 +230,58 @@ def on_startup(self, args):
212230 print (f" FATAL: Could not fetch TLE: { e } " )
213231 sys .exit (1 )
214232
233+ def connect (self ):
234+ """Connect to server. Uses REST mode when OSH_BASE_URL is set, SDK otherwise."""
235+ if self ._rest_mode :
236+ from publishers .bootstrap_helpers import api_get , find_by_uid
237+ sys_id = find_by_uid (self ._base_url , self ._auth , "systems" , self .system_uid )
238+ if not sys_id :
239+ raise RuntimeError (f"System '{ self .system_uid } ' not found on server" )
240+ ds_list = api_get (self ._base_url , f"systems/{ sys_id } /datastreams" , self ._auth )
241+ if ds_list :
242+ for item in ds_list .get ("items" , []):
243+ if item .get ("outputName" ) == "issPosition" :
244+ self ._ds_id = item .get ("id" )
245+ break
246+ if not self ._ds_id :
247+ raise RuntimeError (f"Datastream 'issPosition' not found under system { sys_id } " )
248+ print (f" Connected (REST): sys={ sys_id } ds={ self ._ds_id } " )
249+ else :
250+ return super ().connect ()
251+
252+ def publish_obs (self , obs : dict ) -> bool :
253+ """POST observation. Uses REST when in REST mode, SDK otherwise."""
254+ if self ._rest_mode :
255+ import ssl
256+ url = f"{ self ._base_url } /datastreams/{ self ._ds_id } /observations"
257+
258+ # Go server: coerce numeric timestamp to string
259+ if self ._is_go_server :
260+ r = obs .get ("result" , {})
261+ if "timestamp" in r and not isinstance (r ["timestamp" ], str ):
262+ r ["timestamp" ] = str (r ["timestamp" ])
263+
264+ body = json .dumps (obs ).encode ()
265+ ctx = ssl .create_default_context ()
266+ ctx .check_hostname = False
267+ ctx .verify_mode = ssl .CERT_NONE
268+ req = Request (url , data = body , method = "POST" , headers = {
269+ "Content-Type" : "application/json" ,
270+ "Accept" : "application/json" ,
271+ "Authorization" : self ._auth ,
272+ })
273+ try :
274+ with urlopen (req , timeout = 30 , context = ctx ) as resp :
275+ if resp .status not in (200 , 201 , 204 ):
276+ raise RuntimeError (f"HTTP { resp .status } " )
277+ self .stats ["published" ] += 1
278+ return True
279+ except Exception as e :
280+ self .stats ["errors" ] += 1
281+ raise
282+ else :
283+ return super ().publish_obs (obs )
284+
215285 def fetch (self ) -> Any :
216286 sat = get_satrec ()
217287 now = datetime .now (timezone .utc )
0 commit comments