Skip to content

Commit

Permalink
feat(test_and_documentation): Improving documentation/docstring and s…
Browse files Browse the repository at this point in the history
…tarting fil test files
  • Loading branch information
YpNo committed Oct 12, 2024
1 parent eddc64b commit 85f7995
Show file tree
Hide file tree
Showing 14 changed files with 355 additions and 373 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/python-actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,6 @@ jobs:
uses: microsoft/[email protected]
with:
python_version: '3.12'
pytest: true
pytest: false
testdir: "tests/"
workdir: "."
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/YpNo/arlo-camera-streamer/python-actions.yml)
![CodeQL](https://github.com/YpNo/arlo-camera-streamer/actions/workflows/github-code-scanning/codeql/badge.svg)
![Docker Image CI](https://github.com/YpNo/arlo-camera-streamer/actions/workflows/docker-image.yml/badge.svg)

# arlo-camera-streamer
[![codecov](https://codecov.io/github/YpNo/arlo-camera-streamer/graph/badge.svg?token=1NMSHP7BLW)](https://codecov.io/github/YpNo/arlo-camera-streamer)

> [!IMPORTANT]
> This is a forked project from [arlo-streamer](https://github.com/kaffetorsk/arlo-streamer) project. Reason : Inactivity
# arlo-camera-streamer

Python script that turns arlo cameras into continuous streams through ffmpeg
This allow arlo cameras to be used in the NVR of your choosing. (e.g. [Frigate](https://frigate.video/))

Expand Down
25 changes: 17 additions & 8 deletions base.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class Base(Device):
status_interval (int): Interval of status messages from generator (seconds).
"""

def __init__(self, arlo_base, status_interval):
def __init__(self, arlo_base, status_interval: int):
"""
Initialize the Base instance.
Expand All @@ -25,8 +25,7 @@ def __init__(self, arlo_base, status_interval):
super().__init__(arlo_base, status_interval)
logging.info("Base added: %s", self.name)

# Distributes events to correct handler
async def on_event(self, attr, value):
async def on_event(self, attr: str, value):
"""
Distribute events to the correct handler.
Expand All @@ -36,12 +35,12 @@ async def on_event(self, attr, value):
"""
match attr:
case "activeMode":
self._state_event.set()
self.state_event.set()
logging.info("%s mode: %s", self.name, value)
case _:
pass

def get_status(self):
def get_status(self) -> dict:
"""
Get the status of the base station.
Expand All @@ -50,7 +49,7 @@ def get_status(self):
"""
return {"mode": self._arlo.mode, "siren": self._arlo.siren_state}

async def mqtt_control(self, payload):
async def mqtt_control(self, payload: str):
"""
Handle incoming MQTT commands.
Expand All @@ -63,11 +62,11 @@ async def mqtt_control(self, payload):
payload = json.loads(payload)
for k, v in payload.items():
if k in handlers:
self.event_loop.run_in_executor(None, handlers[k], v)
self._event_loop.run_in_executor(None, handlers[k], v)
except Exception:
logging.warning("%s: Invalid data for MQTT control", self.name)

def set_mode(self, mode):
def set_mode(self, mode: str):
"""
Set the mode of the base station.
Expand Down Expand Up @@ -101,3 +100,13 @@ def set_siren(self, state):
logging.warning("%s: Invalid siren arguments", self.name)
case _:
pass

@property
def state_event(self):
"""
Get the state event object.
Returns:
asyncio.Event: The state event object.
"""
return self._state_event
87 changes: 50 additions & 37 deletions camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
import asyncio
import shlex
import os
from decouple import ( # pylint: disable=import-error # pyright: ignore [reportMissingImports]
config,
)
from decouple import config
from device import Device

DEBUG = config("DEBUG", default=False, cast=bool)
Expand All @@ -33,11 +31,14 @@ class Camera(Device):
"""

# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-public-methods

# Possible states
STATES = ["idle", "streaming"]

def __init__(self, arlo_camera, ffmpeg_out, motion_timeout, status_interval):
def __init__(
self, arlo_camera, ffmpeg_out: str, motion_timeout: int, status_interval: int
):
"""
Initialize the Camera instance.
Expand All @@ -47,7 +48,6 @@ def __init__(self, arlo_camera, ffmpeg_out, motion_timeout, status_interval):
motion_timeout (int): Motion timeout of live stream (seconds).
status_interval (int): Interval of status messages from generator (seconds).
"""

super().__init__(arlo_camera, status_interval)
self.name = arlo_camera.name.replace(" ", "_").lower()
self.ffmpeg_out = shlex.split(ffmpeg_out.format(name=self.name))
Expand All @@ -68,15 +68,13 @@ async def run(self):
Start the camera, wait for it to become available, create event channels,
and listen for events.
"""

while self._arlo.is_unavailable:
await asyncio.sleep(5)
await self.set_state("idle")
asyncio.create_task(self._start_proxy_stream())
asyncio.create_task(self.start_proxy_stream())
await super().run()

# Distributes events to correct handler
async def on_event(self, attr, value):
async def on_event(self, attr: str, value):
"""
Distribute events to the correct handler.
Expand All @@ -95,27 +93,25 @@ async def on_event(self, attr, value):
case _:
pass

# Activates stream on motion
async def on_motion(self, motion):
async def on_motion(self, motion: bool):
"""
Handle motion events. Either start live stream or reset live stream timeout.
Args:
motion (bool): Motion detected status.
"""
self.motion = motion
self._motion_event.set()
self.motion_event.set()
logger.info("%s motion: %s", self.name, motion)
if motion:
await self.set_state("streaming")

else:
if self._timeout_task:
self._timeout_task.cancel()
if not motion:
self._timeout_task = asyncio.create_task(self._stream_timeout())
self._timeout_task = asyncio.create_task(self.stream_timeout())

async def on_arlo_state(self, state):
async def on_arlo_state(self, state: str):
"""
Handle pyaarlo state change, either request stream or handle running stream.
Expand All @@ -124,12 +120,11 @@ async def on_arlo_state(self, state):
"""
if state == "idle":
if self.get_state() == "streaming":
await self._start_stream()
await self.start_stream()
elif state == "userStreamActive" and self.get_state() != "streaming":
await self.set_state("streaming")

# Set state in accordance to STATES
async def set_state(self, new_state):
async def set_state(self, new_state: str):
"""
Set the local state when pyaarlo state changes.
Call the _on_state_change function if the state has changed.
Expand All @@ -140,7 +135,7 @@ async def set_state(self, new_state):
if new_state in self.STATES and new_state != self._state:
self._state = new_state
logger.info("%s state: %s", self.name, new_state)
await self._on_state_change(new_state)
await self.on_state_change(new_state)

def get_state(self):
"""
Expand All @@ -151,24 +146,22 @@ def get_state(self):
"""
return self._state

# Handle internal state change, stop or start stream
async def _on_state_change(self, new_state):
async def on_state_change(self, new_state: str):
"""
Handle internal state change, stop or start stream.
Args:
new_state (str): New state.
"""
self._state_event.set()
self.state_event.set()
match new_state:
case "idle":
self.stop_stream()
asyncio.create_task(self._start_idle_stream())

asyncio.create_task(self.start_idle_stream())
case "streaming":
await self._start_stream()
await self.start_stream()

async def _start_proxy_stream(self):
async def start_proxy_stream(self):
"""Start the proxy stream (continuous video stream from FFmpeg)."""
exit_code = 1
while exit_code > 0:
Expand All @@ -180,7 +173,7 @@ async def _start_proxy_stream(self):
)

if DEBUG:
asyncio.create_task(self._log_stderr(self.proxy_stream, "proxy_stream"))
asyncio.create_task(self.log_stderr(self.proxy_stream, "proxy_stream"))

exit_code = await self.proxy_stream.wait()

Expand All @@ -192,7 +185,7 @@ async def _start_proxy_stream(self):
)
await asyncio.sleep(3)

async def _start_idle_stream(self):
async def start_idle_stream(self):
"""Start the idle picture stream, writing to the proxy stream."""
exit_code = 1
while exit_code > 0:
Expand Down Expand Up @@ -224,7 +217,7 @@ async def _start_idle_stream(self):
)

if DEBUG:
asyncio.create_task(self._log_stderr(self.stream, "idle_stream"))
asyncio.create_task(self.log_stderr(self.stream, "idle_stream"))

exit_code = await self.stream.wait()

Expand All @@ -236,7 +229,7 @@ async def _start_idle_stream(self):
)
await asyncio.sleep(3)

async def _start_stream(self):
async def start_stream(self):
"""
Request stream, grab it, kill idle stream, and start a new FFmpeg instance
writing to the proxy stream.
Expand Down Expand Up @@ -268,9 +261,9 @@ async def _start_stream(self):
)

if DEBUG:
asyncio.create_task(self._log_stderr(self.stream, "live_stream"))
asyncio.create_task(self.log_stderr(self.stream, "live_stream"))

async def _stream_timeout(self):
async def stream_timeout(self):
"""Timeout the live stream after the specified duration."""
await asyncio.sleep(self.timeout)
await self.set_state("idle")
Expand Down Expand Up @@ -308,7 +301,7 @@ def put_picture(self, pic):
except asyncio.QueueFull:
logger.info("picture queue full, ignoring")

def get_status(self):
def get_status(self) -> dict:
"""
Get the camera status information.
Expand All @@ -325,11 +318,11 @@ async def listen_motion(self):
tuple: (name, motion) where name is the camera name and motion is the motion state.
"""
while True:
await self._motion_event.wait()
await self.motion_event.wait()
yield self.name, self.motion
self._motion_event.clear()
self.motion_event.clear()

async def mqtt_control(self, payload):
async def mqtt_control(self, payload: str):
"""
Handle incoming MQTT commands.
Expand All @@ -344,7 +337,7 @@ async def mqtt_control(self, payload):
case "SNAPSHOT":
await self.event_loop.run_in_executor(None, self._arlo.request_snapshot)

async def _log_stderr(self, stream, label):
async def log_stderr(self, stream, label: str):
"""
Continuously read from stderr and log the output.
Expand Down Expand Up @@ -383,3 +376,23 @@ def shutdown(self):
except AttributeError:
# Handle the case when stream is None
logger.debug("Stream for %s is not initialized.", self.name)

@property
def state_event(self):
"""
Get the state event object.
Returns:
asyncio.Event: The state event object.
"""
return self._state_event

@property
def motion_event(self):
"""
Get the motion event object.
Returns:
asyncio.Event: The motion event object.
"""
return self._motion_event
Loading

0 comments on commit 85f7995

Please sign in to comment.