@@ -199,17 +199,22 @@ def map_to_config_file_format(config_dto):
199199 config_dict ["Area_" + str (count )] = map_to_area_file_format (area )
200200 return config_dict
201201
202- def extract_config ():
202+ def extract_config (config_type = 'all' ):
203203 sections = self .config .get_sections ()
204+ if config_type == 'cameras' :
205+ sections = [x for x in sections if x .startswith ("Source" )]
206+ elif config_type == 'areas' :
207+ sections = [x for x in sections if x .startswith ("Area" )]
204208 config = {}
209+
205210 for section in sections :
206211 config [section ] = self .config .get_section_dict (section )
207212 return config
208213
209214 def verify_path (base , camera_id ):
210215 dir_path = os .path .join (base , camera_id )
211216 if not os .path .exists (dir_path ):
212- raise HTTPException (status_code = 404 , detail = f'Camera with id " { camera_id } " does not exist' )
217+ raise HTTPException (status_code = 404 , detail = f'The camera: { camera_id } does not exist' )
213218 return dir_path
214219
215220 def update_config_file (config_dict ):
@@ -239,8 +244,7 @@ def enable_slack(token_config):
239244 logger .info ("Enabling slack notification on processor's config" )
240245 config_dict = dict ()
241246 config_dict ["App" ] = dict ({"EnableSlackNotifications" : "yes" , "SlackChannel" : token_config .channel })
242- self .config .update_config (config_dict )
243- success = restart_processor ()
247+ success = update_and_restart_config (config_dict )
244248
245249 return handle_config_response (config_dict , success )
246250
@@ -257,9 +261,8 @@ def add_slack_channel_to_config(channel):
257261 logger .info ("Adding slack's channel on processor's config" )
258262 config_dict = dict ()
259263 config_dict ["App" ] = dict ({"SlackChannel" : channel })
260- self .config .update_config (config_dict )
261- success = restart_processor ()
262264
265+ success = update_and_restart_config (config_dict )
263266 return handle_config_response (config_dict , success )
264267
265268 def handle_config_response (config , success ):
@@ -274,6 +277,57 @@ def handle_config_response(config, success):
274277 )
275278 return JSONResponse (content = humps .decamelize (config ))
276279
280+ def get_areas ():
281+ config = extract_config (config_type = 'areas' )
282+ return [map_area (x , config ) for x in config .keys ()]
283+
284+ def reestructure_areas (config_dict ):
285+ """Ensure that all [Area_0, Area_1, ...] are consecutive"""
286+ area_names = [x for x in config_dict .keys () if x .startswith ("Area" )]
287+ area_names .sort ()
288+ for index , area_name in enumerate (area_names ):
289+ if f'Area_{ index } ' != area_name :
290+ config_dict [f'Area_{ index } ' ] = config_dict [area_name ]
291+ config_dict .pop (area_name )
292+ return config_dict
293+
294+ def reestructure_cameras (config_dict ):
295+ """Ensure that all [Source_0, Source_1, ...] are consecutive"""
296+ source_names = [x for x in config_dict .keys () if x .startswith ("Source" )]
297+ source_names .sort ()
298+ for index , source_name in enumerate (source_names ):
299+ if f'Source_{ index } ' != source_name :
300+ config_dict [f'Source_{ index } ' ] = config_dict [source_name ]
301+ config_dict .pop (source_name )
302+ return config_dict
303+
304+ def delete_camera_from_areas (camera_id , config_dict ):
305+ areas = {key : config_dict [key ] for key in config_dict .keys () if key .startswith ("Area" )}
306+ for key , area in areas .items ():
307+ cameras = area ['Cameras' ].split (',' )
308+ if camera_id in cameras :
309+ cameras .remove (camera_id )
310+ if len (cameras ) == 0 :
311+ logger .warning (f'After removing the camera "{ camera_id } ", the area "{ area ["Id" ]} - { area ["Name" ]} " was left with no cameras and deleted' )
312+ config_dict .pop (key )
313+ else :
314+ config_dict [key ]['Cameras' ] = "," .join (cameras )
315+
316+ config_dict = reestructure_areas (config_dict )
317+
318+ return config_dict
319+
320+ def get_cameras (options ):
321+ config = extract_config (config_type = 'cameras' )
322+ return [map_camera (x , config , options ) for x in config .keys ()]
323+
324+ def update_and_restart_config (config_dict ):
325+ update_config_file (config_dict )
326+
327+ # TODO: Restart only when necessary, and only the threads that are necessary (for instance to load a new video)
328+ success = restart_processor ()
329+ return success
330+
277331 @app .get ("/process-video-cfg" )
278332 async def process_video_cfg ():
279333 logger .info ("process-video-cfg requests on api" )
@@ -298,12 +352,24 @@ async def get_config(options: Optional[str] = ""):
298352 @app .put ("/config" )
299353 async def update_config (config : ConfigDTO ):
300354 config_dict = map_to_config_file_format (config )
301- update_config_file (config_dict )
302- # TODO: Restart only when necessary, and only the threads that are necessary (for instance to load a new video)
303- success = restart_processor ()
355+
356+ success = update_and_restart_config (config_dict )
304357 return handle_config_response (config_dict , success )
305358
306- @app .post ('/area' )
359+ @app .get ("/areas" )
360+ async def list_areas ():
361+ return {
362+ "areas" : get_areas ()
363+ }
364+
365+ @app .get ("/areas/{area_id}" )
366+ async def get_area (area_id ):
367+ area = next ((area for area in get_areas () if area ['id' ] == area_id ), None )
368+ if not area :
369+ raise HTTPException (status_code = 404 , detail = f'The area: { area_id } does not exist' )
370+ return area
371+
372+ @app .post ('/areas' )
307373 async def create_area (new_area : AreaConfigDTO ):
308374 config_dict = extract_config ()
309375 areas_name = [x for x in config_dict .keys () if x .startswith ("Area" )]
@@ -312,49 +378,42 @@ async def create_area(new_area: AreaConfigDTO):
312378 raise HTTPException (status_code = 400 , detail = "Area already exists" )
313379
314380 cameras = [x for x in config_dict .keys () if x .startswith ("Source" )]
315- cameras = [map_camera (x , config_dict , { } ) for x in cameras ]
381+ cameras = [map_camera (x , config_dict , [] ) for x in cameras ]
316382 camera_ids = [camera ['id' ] for camera in cameras ]
317383 if not all (x in camera_ids for x in new_area .cameras .split (',' )):
318384 non_existent_cameras = set (new_area .cameras .split (',' )) - set (camera_ids )
319385 raise HTTPException (status_code = 404 , detail = f'The cameras: { non_existent_cameras } do not exist' )
320386
321387 config_dict [f'Area_{ len (areas )} ' ] = map_to_area_file_format (new_area )
322- self .config .update_config (config_dict )
323- self .config .reload ()
324388
325- # TODO: Restart only when necessary, and only the threads that are necessary (for instance to load a new video)
326- success = restart_processor ()
389+ success = update_and_restart_config (config_dict )
327390 return handle_config_response (config_dict , success )
328391
329- @app .put ('/area /{area_id}' )
392+ @app .put ('/areas /{area_id}' )
330393 async def edit_area (area_id , edited_area : AreaConfigDTO ):
331394 edited_area .id = area_id
332395 config_dict = extract_config ()
333- areas_name = [x for x in config_dict .keys () if x .startswith ("Area" )]
334- areas = [map_area (x , config_dict ) for x in areas_name ]
396+ area_names = [x for x in config_dict .keys () if x .startswith ("Area" )]
397+ areas = [map_area (x , config_dict ) for x in area_names ]
335398 areas_ids = [area ['id' ] for area in areas ]
336399 try :
337400 index = areas_ids .index (area_id )
338401 except ValueError :
339- raise HTTPException (status_code = 404 , detail = "Area does not exist" )
402+ raise HTTPException (status_code = 404 , detail = f'The area: { area_id } does not exist' )
340403
341404 cameras = [x for x in config_dict .keys () if x .startswith ("Source" )]
342- cameras = [map_camera (x , config_dict , { } ) for x in cameras ]
405+ cameras = [map_camera (x , config_dict , [] ) for x in cameras ]
343406 camera_ids = [camera ['id' ] for camera in cameras ]
344407 if not all (x in camera_ids for x in edited_area .cameras .split (',' )):
345408 non_existent_cameras = set (edited_area .cameras .split (',' )) - set (camera_ids )
346409 raise HTTPException (status_code = 404 , detail = f'The cameras: { non_existent_cameras } do not exist' )
347410
348411 config_dict [f"Area_{ index } " ] = map_to_area_file_format (edited_area )
349412
350- self .config .update_config (config_dict )
351- self .config .reload ()
352-
353- # TODO: Restart only when necessary, and only the threads that are necessary (for instance to load a new video)
354- success = restart_processor ()
413+ success = update_and_restart_config (config_dict )
355414 return handle_config_response (config_dict , success )
356415
357- @app .delete ('/area /{area_id}' )
416+ @app .delete ('/areas /{area_id}' )
358417 async def delete_area (area_id ):
359418 config_dict = extract_config ()
360419 areas_name = [x for x in config_dict .keys () if x .startswith ("Area" )]
@@ -363,17 +422,77 @@ async def delete_area(area_id):
363422 try :
364423 index = areas_ids .index (area_id )
365424 except ValueError :
366- raise HTTPException (status_code = 404 , detail = "Area does not exist" )
425+ raise HTTPException (status_code = 404 , detail = f'The area: { area_id } does not exist' )
367426
368427 config_dict .pop (f'Area_{ index } ' )
369- self .config .update_config (config_dict )
370- self .config .reload ()
428+ config_dict = reestructure_areas ((config_dict ))
371429
372- # TODO: Restart only when necessary, and only the threads that are necessary (for instance to load a new video)
373- success = restart_processor ()
430+ success = update_and_restart_config (config_dict )
374431 return handle_config_response (config_dict , success )
375432
376- @app .get ("/{camera_id}/image" , response_model = ImageModel )
433+ @app .get ("/cameras" )
434+ async def list_cameras (options : Optional [str ] = "" ):
435+ return {
436+ "cameras" : get_cameras (options )
437+ }
438+
439+ @app .get ("/cameras/{camera_id}" )
440+ async def get_camera (camera_id ):
441+ camera = next ((camera for camera in get_cameras (['withImage' ]) if camera ['id' ] == camera_id ), None )
442+ if not camera :
443+ raise HTTPException (status_code = 404 , detail = f'The camera: { camera_id } does not exist' )
444+ return camera
445+
446+ @app .post ("/cameras" )
447+ async def create_camera (new_camera : SourceConfigDTO ):
448+ config_dict = extract_config ()
449+ cameras_name = [x for x in config_dict .keys () if x .startswith ("Source" )]
450+ cameras = [map_camera (x , config_dict , []) for x in cameras_name ]
451+ if new_camera .id in [camera ['id' ] for camera in cameras ]:
452+ raise HTTPException (status_code = 400 , detail = "Camera already exists" )
453+
454+ config_dict [f'Source_{ len (cameras )} ' ] = map_to_camera_file_format (new_camera )
455+
456+ success = update_and_restart_config (config_dict )
457+ return handle_config_response (config_dict , success )
458+
459+ @app .put ("/cameras/{camera_id}" )
460+ async def edit_camera (camera_id , edited_camera : SourceConfigDTO ):
461+ edited_camera .id = camera_id
462+ config_dict = extract_config ()
463+ camera_names = [x for x in config_dict .keys () if x .startswith ("Source" )]
464+ cameras = [map_camera (x , config_dict , []) for x in camera_names ]
465+ cameras_ids = [camera ['id' ] for camera in cameras ]
466+ try :
467+ index = cameras_ids .index (camera_id )
468+ except ValueError :
469+ raise HTTPException (status_code = 404 , detail = f'The camera: { camera_id } does not exist' )
470+
471+ config_dict [f"Source_{ index } " ] = map_to_camera_file_format (edited_camera )
472+
473+ success = update_and_restart_config (config_dict )
474+ return handle_config_response (config_dict , success )
475+
476+ @app .delete ("/cameras/{camera_id}" )
477+ async def delete_camera (camera_id ):
478+ config_dict = extract_config ()
479+ camera_names = [x for x in config_dict .keys () if x .startswith ("Source" )]
480+ cameras = [map_area (x , config_dict ) for x in camera_names ]
481+ cameras_ids = [camera ['id' ] for camera in cameras ]
482+ try :
483+ index = cameras_ids .index (camera_id )
484+ except ValueError :
485+ raise HTTPException (status_code = 404 , detail = f'The camera: { camera_id } does not exist' )
486+
487+ config_dict = delete_camera_from_areas (camera_id , config_dict )
488+
489+ config_dict .pop (f'Source_{ index } ' )
490+ config_dict = reestructure_cameras ((config_dict ))
491+
492+ success = update_and_restart_config (config_dict )
493+ return handle_config_response (config_dict , success )
494+
495+ @app .get ("/cameras/{camera_id}/image" , response_model = ImageModel )
377496 async def get_camera_image (camera_id ):
378497 dir_path = verify_path (self .config .get_section_dict ("App" )["ScreenshotsDirectory" ], camera_id )
379498 with open (f'{ dir_path } /default.jpg' , "rb" ) as image_file :
@@ -382,7 +501,7 @@ async def get_camera_image(camera_id):
382501 "image" : encoded_string
383502 }
384503
385- @app .put ("/{camera_id}/image" )
504+ @app .put ("/cameras/ {camera_id}/image" )
386505 async def replace_camera_image (camera_id , body : ImageModel ):
387506 dir_path = verify_path (self .config .get_section_dict ("App" )["ScreenshotsDirectory" ], camera_id )
388507 try :
@@ -393,23 +512,20 @@ async def replace_camera_image(camera_id, body: ImageModel):
393512 except Exception :
394513 return HTTPException (status_code = 400 , detail = "Invalid image format" )
395514
396- @app .post ("/{camera_id}/homography_matrix" )
515+ @app .post ("/cameras/ {camera_id}/homography_matrix" )
397516 async def config_calibrated_distance (camera_id , body : ConfigHomographyMatrix ):
398- sources = self .config .get_video_sources ()
399- dir_source = list (filter (lambda source : source ['id' ] == camera_id , sources ))
400- if dir_source is None or len (dir_source ) != 1 :
401- raise HTTPException (status_code = 404 , detail = f'Camera with id "{ camera_id } " does not exist' )
402- dir_source = dir_source [0 ]
517+ dir_source = next ((source for source in self .config .get_video_sources () if source ['id' ] == camera_id ), None )
518+ if not dir_source :
519+ raise HTTPException (status_code = 404 , detail = f'The camera: { camera_id } does not exist' )
403520 dir_path = get_camera_calibration_path (self .config , camera_id )
404521 compute_and_save_inv_homography_matrix (points = body , destination = dir_path )
405522 sections = self .config .get_sections ()
406523 config_dict = {}
407524 for section in sections :
408525 config_dict [section ] = self .config .get_section_dict (section )
409526 config_dict [dir_source ['section' ]]['DistMethod' ] = 'CalibratedDistance'
410- update_config_file (config_dict )
411- success = restart_processor ()
412527
528+ success = update_and_restart_config (config_dict )
413529 return handle_config_response (config_dict , success )
414530
415531 @app .get ("/slack/is-enabled" )
0 commit comments