Coverage for src/rift_console/ciarc_api.py: 0%

192 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-08 09:36 +0000

1from typing import Any, Optional 

2import requests 

3import datetime 

4 

5from loguru import logger 

6 

7import shared.constants as con 

8from shared.models import ( 

9 Achievement, 

10 BeaconObjective, 

11 CameraAngle, 

12 BaseTelemetry, 

13 State, 

14 Slot, 

15 ZonedObjective, 

16 live_utc, 

17 HttpCode, 

18) 

19 

20def console_api( 

21 method: HttpCode, 

22 endpoint: str, 

23 params: dict[str, Any] = {}, 

24 json: dict[str, Any] = {}, 

25 files: dict[str, Any] = {}, 

26) -> Any: 

27 """Wrapper for ciarc api with error handling.""" 

28 try: 

29 with requests.Session() as s: 

30 match method: 

31 case HttpCode.GET: 

32 r = s.get(endpoint, timeout=10) 

33 case HttpCode.PUT: 

34 r = s.put(endpoint, params=params, json=json, timeout=5) 

35 case HttpCode.DELETE: 

36 r = s.delete(endpoint, params=params, timeout=5) 

37 case HttpCode.POST: 

38 r = s.post(endpoint, params=params, files=files) 

39 

40 except requests.exceptions.ConnectionError: 

41 logger.error("Console: ConnectionError - possible no VPN?") 

42 return {} 

43 except requests.exceptions.ReadTimeout: 

44 logger.error("Console: Timeout error - possible no VPN?") 

45 return {} 

46 

47 match r.status_code: 

48 case 200: 

49 logger.debug(f"Console: received from API - {type(r.json())} - {r.json()}") 

50 return r.json() 

51 case 405: 

52 # this happens with illegal request, for example GET instead of PUT 

53 logger.error( 

54 f"Console: API Not Allowed {r.status_code} - {type(r.json())} - {r.json()}" 

55 ) 

56 return {} 

57 case 422: 

58 # this happens for an illegal control request, for example accelerating while not in acquisition 

59 logger.warning( 

60 f"Console: API Unprocessable Content- {r.status_code} - {type(r.json())} - {r.json()}." 

61 ) 

62 return {} 

63 case 500: 

64 # this happens with an bug on the api side? Should not appear anymore 

65 logger.warning( 

66 f"Console: API File not found - {r.status_code} - {type(r.json())} - {r.json()}." 

67 ) 

68 return {} 

69 case _: 

70 # unknow error? 

71 logger.warning( 

72 f"Console: could not contact satellite - {r.status_code} - {type(r.json())} - {r.json()}." 

73 ) 

74 return {} 

75 

76 

77def console_api_image(angle: CameraAngle) -> Optional[str]: 

78 """Wrapper for CIARC API with images, since it has a slighly different syntax.""" 

79 try: 

80 with requests.Session() as s: 

81 r = s.get(con.IMAGE_ENDPOINT, timeout=10) 

82 except requests.exceptions.ConnectionError: 

83 logger.error("Console: ConnectionError - possible no VPN?") 

84 return None 

85 except requests.exceptions.ReadTimeout: 

86 logger.error("Console: Timeout error - possible no VPN?") 

87 return None 

88 

89 match r.status_code: 

90 case 200: 

91 img_timestamp = datetime.datetime.fromisoformat( 

92 r.headers.get("image-timestamp") or "" 

93 ).strftime("%Y-%m-%dT%H:%M:%S") 

94 with open( 

95 con.CONSOLE_LIVE_PATH + "live_" + angle + "_" + img_timestamp + ".png", 

96 "wb", 

97 ) as f: 

98 f.write(r.content) 

99 logger.warning(f"Console: received Image - {img_timestamp}") 

100 return img_timestamp 

101 case 400: 

102 logger.warning(f"Console: Bad request - {type(r.json())} - {r.json()}") 

103 return None 

104 case _: 

105 logger.warning(f"Console: Unkown error: - {type(r.json())} - {r}") 

106 return None 

107 

108 

109def reset() -> None: 

110 """Reset Simulation.""" 

111 console_api(method=HttpCode.GET, endpoint=con.RESET_ENDPOINT) 

112 return 

113 

114def save_backup() -> datetime.datetime: 

115 """Save backup of simulation.""" 

116 console_api(method=HttpCode.GET, endpoint=con.BACKUP_ENDPOINT) 

117 t = live_utc() 

118 logger.info("Console: saving satellite state.") 

119 

120 return t 

121 

122def load_backup(last_backup_date: Optional[datetime.datetime]) -> None: 

123 """Load backup of simulation.""" 

124 console_api(method=HttpCode.PUT, endpoint=con.BACKUP_ENDPOINT) 

125 logger.info(f"Console: restoring satellite state from {last_backup_date}.") 

126 

127 return 

128 

129def change_simulation_env( 

130 is_network_simulation: bool = False, user_speed_multiplier: int = 1 

131) -> None: 

132 """Change simspeed or network simulation.""" 

133 params = { 

134 "is_network_simulation": str(is_network_simulation).lower(), 

135 "user_speed_multiplier": str(user_speed_multiplier), 

136 } 

137 console_api(method=HttpCode.PUT, endpoint=con.SIMULATION_ENDPOINT, params=params) 

138 logger.info( 

139 f"Console: simulation speed set to {user_speed_multiplier} - network simulation is {is_network_simulation}." 

140 ) 

141 

142 return 

143 

144 

145def update_api() -> ( 

146 Optional[ 

147 tuple[ 

148 int, 

149 list[Slot], 

150 list[ZonedObjective], 

151 list[BeaconObjective], 

152 list[Achievement], 

153 ] 

154 ] 

155): 

156 """Pull status like slots and objectives, that are available even outside comms window.""" 

157 s = console_api(method=HttpCode.GET, endpoint=con.SLOTS_ENDPOINT) 

158 o = console_api(method=HttpCode.GET, endpoint=con.OBJECTIVE_ENDPOINT) 

159 a = console_api(method=HttpCode.GET, endpoint=con.ACHIEVEMENTS_ENDPOINT) 

160 if s and o and a: 

161 (slots_used, slots) = Slot.parse_api(s) 

162 zoned_objectives = ZonedObjective.parse_api(o) 

163 beacon_objectives = BeaconObjective.parse_api(o) 

164 achievements = Achievement.parse_api(a) 

165 logger.info( 

166 f"Updated slots, objectives, achievments, used {slots_used} slots so far." 

167 ) 

168 return (slots_used, slots, zoned_objectives, beacon_objectives, achievements) 

169 else: 

170 logger.warning("Could not update slots, objectives, achievments.") 

171 return None 

172 

173 

174def live_telemetry() -> Optional[BaseTelemetry]: 

175 """Pulls /observation.""" 

176 d = console_api(method=HttpCode.GET, endpoint=con.OBSERVATION_ENDPOINT) 

177 if d: 

178 b = BaseTelemetry(**d) 

179 logger.info(f"Console: received live telemetry\n{b}.") 

180 return b 

181 else: 

182 logger.warning("Live telemtry failed.") 

183 return None 

184 

185 

186def change_angle(angle: CameraAngle) -> Any: 

187 """Change camera angle, keep veloctiy constant.""" 

188 obs = console_api(method=HttpCode.GET, endpoint=con.OBSERVATION_ENDPOINT) 

189 if not obs: 

190 logger.warning("Console: no telemetry available, could not change camera angle") 

191 return {} 

192 json = { 

193 "vel_x": obs["vx"], 

194 "vel_y": obs["vy"], 

195 "camera_angle": angle, 

196 "state": obs["state"], 

197 } 

198 d = console_api(method=HttpCode.PUT, endpoint=con.CONTROL_ENDPOINT, json=json) 

199 

200 if d and d["camera_angle"] == angle: 

201 logger.info(f"Console: angle changed to {d["camera_angle"]}.") 

202 else: 

203 logger.warning("Console: could not change angle, not in acquisition?") 

204 return {} 

205 

206 return d 

207 

208 

209def change_state(state: State) -> Any: 

210 """Change State.""" 

211 obs = console_api(method=HttpCode.GET, endpoint=con.OBSERVATION_ENDPOINT) 

212 if not obs: 

213 logger.warning("Console: no telemetry available, could not change camera angle") 

214 return 

215 json = { 

216 "vel_x": obs["vx"], 

217 "vel_y": obs["vy"], 

218 "camera_angle": obs["angle"], 

219 "state": state, 

220 } 

221 d = console_api(method=HttpCode.PUT, endpoint=con.CONTROL_ENDPOINT, json=json) 

222 

223 if d and d["state"] == state: 

224 logger.info(f"Console: state changed to {d["state"]}.") 

225 else: 

226 logger.warning("Console: could not change state, not in acquisition?") 

227 return {} 

228 

229 return d 

230 

231 

232def change_velocity(vel_x: float, vel_y: float) -> Any: 

233 """Change velocity of MELVIN.""" 

234 obs = console_api(method=HttpCode.GET, endpoint=con.OBSERVATION_ENDPOINT) 

235 if not obs: 

236 logger.warning("Console: no telemetry available, could not change camera angle") 

237 return {} 

238 json = { 

239 "vel_x": vel_x, 

240 "vel_y": vel_y, 

241 "camera_angle": obs["angle"], 

242 "state": obs["state"], 

243 } 

244 d = console_api(method=HttpCode.PUT, endpoint=con.CONTROL_ENDPOINT, json=json) 

245 

246 if d and d["vel_x"] == vel_x and d["vel_y"] == vel_y: 

247 logger.info(f"Console: velocity changed to ({d["vel_x"]},{d["vel_y"]}).") 

248 return d 

249 else: 

250 logger.warning("Console: could not change velocity, not in acquisition?") 

251 return {} 

252 

253 

254def book_slot(slot_id: int, enabled: bool) -> None: 

255 """Book coms slot.""" 

256 params = { 

257 "slot_id": slot_id, 

258 "enabled": str(enabled).lower(), 

259 } 

260 d = console_api(method=HttpCode.PUT, endpoint=con.SLOTS_ENDPOINT, params=params) 

261 

262 if d: 

263 if d["enabled"]: 

264 logger.info(f"Console: booked communication slot {d["id"]}.") 

265 else: 

266 logger.info(f"Console: cancled communication slot {d["id"]}") 

267 else: 

268 logger.warning("Console: could not book slot, not in acquisition?") 

269 

270 

271def delete_objective(id: int) -> None: 

272 """Delete objective (only used while testing).""" 

273 params = { 

274 "id": str(id), 

275 } 

276 d = console_api( 

277 method=HttpCode.DELETE, endpoint=con.OBJECTIVE_ENDPOINT, params=params 

278 ) 

279 if d: 

280 logger.info(f"Console: removed objective with id - {id}.") 

281 else: 

282 logger.warning(f"Console: could not delete objective with id - {id}") 

283 

284 

285def add_modify_zoned_objective( 

286 id: int, 

287 name: str, 

288 start: datetime.datetime, 

289 end: datetime.datetime, 

290 zone: tuple[int, int, int, int], 

291 optic_required: CameraAngle, 

292 coverage_required: float, 

293 description: str, 

294 secret: bool, 

295) -> bool: 

296 """Add zoned or hidden objectives for testing.""" 

297 json = { 

298 "zoned_objectives": [ 

299 { 

300 "id": id, 

301 "name": name, 

302 "start": start.replace(tzinfo=datetime.timezone.utc).isoformat(), 

303 "end": end.replace(tzinfo=datetime.timezone.utc).isoformat(), 

304 "decrease_rate": 0.99, # hardcoded since not in use 

305 "zone": [zone[0], zone[1], zone[2], zone[3]], 

306 "optic_required": optic_required, 

307 "coverage_required": coverage_required, 

308 "description": description, 

309 "sprite": "string", # hardcoded since not in use 

310 "secret": secret, 

311 } 

312 ], 

313 "beacon_objectives": [], 

314 } 

315 

316 d = console_api(method=HttpCode.PUT, endpoint=con.OBJECTIVE_ENDPOINT, json=json) 

317 

318 if d: 

319 logger.info(f"Console: add/modifyed zoned objective {id}/{name}.") 

320 return True 

321 else: 

322 logger.warning(f"Console: could not add/modifyed zoned objective {id}/{name}") 

323 return False 

324 

325 

326def add_modify_ebt_objective( 

327 id: int, 

328 name: str, 

329 start: datetime.datetime, 

330 end: datetime.datetime, 

331 description: str, 

332 beacon_height: int, 

333 beacon_width: int, 

334) -> bool: 

335 """Add EBT objectives for testing.""" 

336 json = { 

337 "zoned_objectives": [], 

338 "beacon_objectives": [ 

339 { 

340 "id": id, 

341 "name": name, 

342 "start": start.replace(tzinfo=datetime.timezone.utc).isoformat(), 

343 "end": end.replace(tzinfo=datetime.timezone.utc).isoformat(), 

344 "decrease_rate": 0.99, # hardcoded since not in use 

345 "description": description, 

346 "beacon_height": beacon_height, 

347 "beacon_width": beacon_width, 

348 "attempts_made": 0, # did not change anything in API 

349 } 

350 ], 

351 } 

352 

353 d = console_api(method=HttpCode.PUT, endpoint=con.OBJECTIVE_ENDPOINT, json=json) 

354 

355 if d: 

356 logger.info(f"Console: add/modifyed ebt objective {id}/{name}.") 

357 return True 

358 else: 

359 logger.warning(f"Console: could not add/modifyed ebt objective {id}/{name}") 

360 return False 

361 

362 

363def send_beacon(beacon_id: int, height: int, width: int) -> Any: 

364 """Guess a EBT position.""" 

365 params = {"beacon_id": beacon_id, "height": height, "width": width} 

366 d = console_api(method=HttpCode.PUT, endpoint=con.BEACON_ENDPOINT, params=params) 

367 if d: 

368 logger.info(f"Console: send_beacon - {d}.") 

369 return d 

370 else: 

371 logger.warning(f"Console: could not send_beacon - {id}") 

372 return {} 

373 

374 

375def upload_worldmap(image_path: str) -> Any: 

376 """Upload a worldmap""" 

377 files = {"image": (image_path, open(image_path, "rb"), "image/png")} 

378 d = console_api(method=HttpCode.POST, endpoint=con.DAILYMAP_ENDPOINT, files=files) 

379 if d: 

380 logger.info(f"Console: Uploaded world map - {d}.") 

381 # shutil.copyfile( 

382 # image_path, 

383 # "src/rift_console/static/media/" 

384 # + live_utc().strftime("%d-%m-%Y") 

385 # + "worldmap.png", 

386 # ) 

387 return d 

388 else: 

389 logger.warning("Console: could not upload world map") 

390 return "" 

391 

392 

393def upload_objective(image_path: str, objective_id: int) -> Any: 

394 """Upload an images of an objective.""" 

395 params = { 

396 "objective_id": objective_id, 

397 } 

398 files = {"image": (image_path, open(image_path, "rb"), "image/png")} 

399 d = console_api( 

400 method=HttpCode.POST, endpoint=con.IMAGE_ENDPOINT, params=params, files=files 

401 ) 

402 if d: 

403 logger.info(f"Console: Uploaded objective - {d}.") 

404 # shutil.copyfile( 

405 # image_path, 

406 # con.CONSOLE_STICHED_PATH + str(objective_id) + "objective.png", 

407 # ) 

408 return d 

409 else: 

410 logger.warning("Console: could not upload objective") 

411 return ""