Skip to content

Commit

Permalink
Imprrove day check (#223)
Browse files Browse the repository at this point in the history
* new day check

* update tests
  • Loading branch information
MateoLostanlen committed Jul 30, 2024
1 parent 9e70b17 commit 4742146
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 29 deletions.
106 changes: 77 additions & 29 deletions pyroengine/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,16 @@ def is_day_time(cache, frame, strategy, delta=0):
return is_day


async def capture_camera_image(camera: ReolinkCamera, image_queue: asyncio.Queue) -> None:
async def capture_camera_image(camera: ReolinkCamera, image_queue: asyncio.Queue) -> bool:
"""
Captures an image from the camera and puts it into a queue.
Captures an image from the camera and puts it into a queue. Returns whether it is daytime for this camera.
Args:
camera (ReolinkCamera): The camera instance.
image_queue (asyncio.Queue): The queue to put the captured image.
Returns:
bool: True if it is daytime according to this camera, False otherwise.
"""
cam_id = camera.ip_address
try:
Expand All @@ -75,16 +78,21 @@ async def capture_camera_image(camera: ReolinkCamera, image_queue: asyncio.Queue
# Move camera to the next pose to avoid waiting
next_pos_id = camera.cam_poses[(idx + 1) % len(camera.cam_poses)]
camera.move_camera("ToPos", idx=int(next_pos_id), speed=50)
if frame is not None and is_day_time(None, frame, "ir"):
if frame is not None:
await image_queue.put((cam_id, frame))
await asyncio.sleep(0) # Yield control
if not is_day_time(None, frame, "ir"):
return False
else:
frame = camera.capture()
if frame is not None:
await image_queue.put((cam_id, frame))
await asyncio.sleep(0) # Yield control
if not is_day_time(None, frame, "ir"):
return False
except Exception as e:
logging.exception(f"Error during image capture from camera {cam_id}: {e}")
return True


class SystemController:
Expand All @@ -106,17 +114,21 @@ def __init__(self, engine: Engine, cameras: List[ReolinkCamera]) -> None:
"""
self.engine = engine
self.cameras = cameras
self.day_time = True
self.is_day = True

async def capture_images(self, image_queue: asyncio.Queue) -> None:
async def capture_images(self, image_queue: asyncio.Queue) -> bool:
"""
Captures images from all cameras using asyncio.
Args:
image_queue (asyncio.Queue): The queue to put the captured images.
Returns:
bool: True if it is daytime according to all cameras, False otherwise.
"""
tasks = [capture_camera_image(camera, image_queue) for camera in self.cameras]
await asyncio.gather(*tasks)
day_times = await asyncio.gather(*tasks)
return all(day_times)

async def analyze_stream(self, image_queue: asyncio.Queue) -> None:
"""
Expand All @@ -137,58 +149,94 @@ async def analyze_stream(self, image_queue: asyncio.Queue) -> None:
finally:
image_queue.task_done() # Mark the task as done

async def run(self, period: int = 30, send_alerts: bool = True) -> None:
async def night_mode(self) -> bool:
"""
Checks if it is nighttime for any camera.
Returns:
bool: True if it is daytime for all cameras, False otherwise.
"""
for camera in self.cameras:
cam_id = camera.ip_address
try:
if camera.cam_type == "ptz":
for idx, pose_id in enumerate(camera.cam_poses):
cam_id = f"{camera.ip_address}_{pose_id}"
frame = camera.capture()
# Move camera to the next pose to avoid waiting
next_pos_id = camera.cam_poses[(idx + 1) % len(camera.cam_poses)]
camera.move_camera("ToPos", idx=int(next_pos_id), speed=50)
if not is_day_time(None, frame, "ir"):
return False
else:
frame = camera.capture()
if not is_day_time(None, frame, "ir"):
return False
except Exception as e:
logging.exception(f"Error during image capture from camera {cam_id}: {e}")
return True

async def run(self, period: int = 30, send_alerts: bool = True) -> bool:
"""
Captures and analyzes all camera streams, then processes alerts.
Args:
period (int): The time period between captures in seconds.
send_alerts (bool): Boolean to activate / deactivate alert sending
send_alerts (bool): Boolean to activate / deactivate alert sending.
Returns:
bool: True if it is daytime according to all cameras, False otherwise.
"""
try:
image_queue: asyncio.Queue[Any] = asyncio.Queue()

if self.day_time:
image_queue: asyncio.Queue[Any] = asyncio.Queue()

# Start the image processor task
processor_task = asyncio.create_task(self.analyze_stream(image_queue))
# Start the image processor task
processor_task = asyncio.create_task(self.analyze_stream(image_queue))

# Capture images concurrently
await self.capture_images(image_queue)
# Capture images concurrently
self.is_day = await self.capture_images(image_queue)

# Wait for the image processor to finish processing
await image_queue.join() # Ensure all tasks are marked as done
# Wait for the image processor to finish processing
await image_queue.join() # Ensure all tasks are marked as done

# Signal the image processor to stop processing
await image_queue.put(None)
await processor_task # Ensure the processor task completes
# Signal the image processor to stop processing
await image_queue.put(None)
await processor_task # Ensure the processor task completes

# Process alerts
# Process alerts
if send_alerts:
try:
if send_alerts:
self.engine._process_alerts()
self.engine._process_alerts()
except Exception as e:
logging.error(f"Error processing alerts: {e}")

return self.is_day

except Exception as e:
logging.warning(f"Analyze stream error: {e}")
return True

async def main_loop(self, period: int, send_alerts: bool = True) -> None:
"""
Main loop to capture and process images at regular intervals.
Args:
period (int): The time period between captures in seconds.
send_alerts (bool): Boolean to activate / deactivate alert sending
send_alerts (bool): Boolean to activate / deactivate alert sending.
"""
while True:
start_ts = time.time()
await self.run(period, send_alerts)
# Sleep only once all images are processed
loop_time = time.time() - start_ts
sleep_time = max(period - (loop_time), 0)
logging.info(f"Loop run under {loop_time:.2f} seconds, sleeping for {sleep_time:.2f}")
await asyncio.sleep(sleep_time)
if not self.is_day:
while not await self.night_mode():
logging.info("Nighttime detected by at least one camera, sleeping for 1 hour.")
await asyncio.sleep(3600) # Sleep for 1 hour
else:
# Sleep only once all images are processed
loop_time = time.time() - start_ts
sleep_time = max(period - (loop_time), 0)
logging.info(f"Loop run under {loop_time:.2f} seconds, sleeping for {sleep_time:.2f}")
await asyncio.sleep(sleep_time)

def __repr__(self) -> str:
"""
Expand Down
25 changes: 25 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ def mock_cameras_ptz(mock_wildfire_image):
return [camera]


@pytest.fixture
def mock_cameras_ptz_night():
camera = MagicMock()
camera.capture.return_value = Image.new("RGB", (100, 100), (255, 255, 255)) # Mock captured image
camera.cam_type = "ptz"
camera.cam_poses = [1, 2]
camera.ip_address = "192.168.1.1"
return [camera]


@pytest.fixture
def system_controller(mock_engine, mock_cameras):
return SystemController(engine=mock_engine, cameras=mock_cameras)
Expand All @@ -44,6 +54,21 @@ def system_controller_ptz(mock_engine, mock_cameras_ptz):
return SystemController(engine=mock_engine, cameras=mock_cameras_ptz)


@pytest.fixture
def system_controller_ptz_night(mock_engine, mock_cameras_ptz_night):
return SystemController(engine=mock_engine, cameras=mock_cameras_ptz_night)


@pytest.mark.asyncio
async def test_night_mode(system_controller):
assert await system_controller.night_mode()


@pytest.mark.asyncio
async def test_night_mode_ptz(system_controller_ptz_night):
assert not await system_controller_ptz_night.night_mode()


def test_is_day_time_ir_strategy(mock_wildfire_image):
# Use day image
assert is_day_time(None, mock_wildfire_image, "ir")
Expand Down

0 comments on commit 4742146

Please sign in to comment.