Improvements to motion detection and download methods, add to examples
This commit is contained in:
43
examples/download_motions.py
Normal file
43
examples/download_motions.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import os
|
||||||
|
from configparser import RawConfigParser
|
||||||
|
from datetime import datetime as dt, timedelta
|
||||||
|
from reolink_api import Camera
|
||||||
|
|
||||||
|
|
||||||
|
def read_config(props_path: str) -> dict:
|
||||||
|
"""Reads in a properties file into variables.
|
||||||
|
|
||||||
|
NB! this config file is kept out of commits with .gitignore. The structure of this file is such:
|
||||||
|
# secrets.cfg
|
||||||
|
[camera]
|
||||||
|
ip={ip_address}
|
||||||
|
username={username}
|
||||||
|
password={password}
|
||||||
|
"""
|
||||||
|
config = RawConfigParser()
|
||||||
|
assert os.path.exists(props_path), f"Path does not exist: {props_path}"
|
||||||
|
config.read(props_path)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
# Read in your ip, username, & password
|
||||||
|
# (NB! you'll likely have to create this file. See tests/test_camera.py for details on structure)
|
||||||
|
config = read_config('../secrets.cfg')
|
||||||
|
|
||||||
|
ip = config.get('camera', 'ip')
|
||||||
|
un = config.get('camera', 'username')
|
||||||
|
pw = config.get('camera', 'password')
|
||||||
|
|
||||||
|
# Connect to camera
|
||||||
|
cam = Camera(ip, un, pw)
|
||||||
|
|
||||||
|
start = (dt.now() - timedelta(hours=1))
|
||||||
|
end = dt.now()
|
||||||
|
# Collect motion events between these timestamps for substream
|
||||||
|
processed_motions = cam.get_motion_files(start=start, end=end, streamtype='sub')
|
||||||
|
|
||||||
|
dl_dir = os.path.join(os.path.expanduser('~'), 'Downloads')
|
||||||
|
for i, motion in enumerate(processed_motions):
|
||||||
|
fname = motion['filename']
|
||||||
|
# Download the mp4
|
||||||
|
resp = cam.get_file(fname, output_path=os.path.join(dl_dir, f'motion_event_{i}.mp4'))
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import requests
|
||||||
from reolink_api.resthandle import Request
|
from reolink_api.resthandle import Request
|
||||||
from .alarm import AlarmAPIMixin
|
from .alarm import AlarmAPIMixin
|
||||||
from .device import DeviceAPIMixin
|
from .device import DeviceAPIMixin
|
||||||
@@ -109,7 +110,23 @@ class APIHandler(AlarmAPIMixin,
|
|||||||
try:
|
try:
|
||||||
if self.token is None:
|
if self.token is None:
|
||||||
raise ValueError("Login first")
|
raise ValueError("Login first")
|
||||||
response = Request.post(self.url, data=data, params=params)
|
if command == 'Download':
|
||||||
|
# Special handling for downloading an mp4
|
||||||
|
# Pop the filepath from data
|
||||||
|
tgt_filepath = data[0].pop('filepath')
|
||||||
|
# Apply the data to the params
|
||||||
|
params.update(data[0])
|
||||||
|
with requests.get(self.url, params=params, stream=True) as req:
|
||||||
|
if req.status_code == 200:
|
||||||
|
with open(tgt_filepath, 'wb') as f:
|
||||||
|
f.write(req.content)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f'Error received: {req.status_code}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
else:
|
||||||
|
response = Request.post(self.url, data=data, params=params)
|
||||||
return response.json()
|
return response.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Command {command} failed: {e}")
|
print(f"Command {command} failed: {e}")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
class DownloadAPIMixin:
|
class DownloadAPIMixin:
|
||||||
"""API calls for downloading video files."""
|
"""API calls for downloading video files."""
|
||||||
def get_file(self, filename: str) -> object:
|
def get_file(self, filename: str, output_path: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Download the selected video file
|
Download the selected video file
|
||||||
:return: response json
|
:return: response json
|
||||||
@@ -9,7 +9,10 @@ class DownloadAPIMixin:
|
|||||||
{
|
{
|
||||||
"cmd": "Download",
|
"cmd": "Download",
|
||||||
"source": filename,
|
"source": filename,
|
||||||
"output": filename
|
"output": filename,
|
||||||
|
"filepath": output_path
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
return self._execute_command('Download', body)
|
resp = self._execute_command('Download', body)
|
||||||
|
|
||||||
|
return resp
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
|
from typing import Union, List, Dict
|
||||||
from datetime import datetime as dt
|
from datetime import datetime as dt
|
||||||
|
|
||||||
|
|
||||||
|
# Type hints for input and output of the motion api response
|
||||||
|
RAW_MOTION_LIST_TYPE = List[Dict[str, Union[str, int, Dict[str, str]]]]
|
||||||
|
PROCESSED_MOTION_LIST_TYPE = List[Dict[str, Union[str, dt]]]
|
||||||
|
|
||||||
|
|
||||||
class MotionAPIMixin:
|
class MotionAPIMixin:
|
||||||
"""API calls for past motion alerts."""
|
"""API calls for past motion alerts."""
|
||||||
def get_motion_files(self, start: dt, end: dt = dt.now(),
|
def get_motion_files(self, start: dt, end: dt = dt.now(),
|
||||||
streamtype: str = 'main') -> object:
|
streamtype: str = 'sub') -> PROCESSED_MOTION_LIST_TYPE:
|
||||||
"""
|
"""
|
||||||
Get the timestamps and filenames of motion detection events for the time range provided.
|
Get the timestamps and filenames of motion detection events for the time range provided.
|
||||||
|
|
||||||
@@ -38,4 +44,36 @@ class MotionAPIMixin:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
body = [{"cmd": "Search", "action": 1, "param": search_params}]
|
body = [{"cmd": "Search", "action": 1, "param": search_params}]
|
||||||
return self._execute_command('Search', body)
|
|
||||||
|
resp = self._execute_command('Search', body)[0]
|
||||||
|
files = resp['value']['SearchResult']['File']
|
||||||
|
if len(files) > 0:
|
||||||
|
# Begin processing files
|
||||||
|
processed_files = self._process_motion_files(files)
|
||||||
|
return processed_files
|
||||||
|
return []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _process_motion_files(motion_files: RAW_MOTION_LIST_TYPE) -> PROCESSED_MOTION_LIST_TYPE:
|
||||||
|
"""Processes raw list of dicts containing motion timestamps
|
||||||
|
and the filename associated with them"""
|
||||||
|
# Process files
|
||||||
|
processed_motions = []
|
||||||
|
replace_fields = {'mon': 'month', 'sec': 'second', 'min': 'minute'}
|
||||||
|
for file in motion_files:
|
||||||
|
time_range = {}
|
||||||
|
for x in ['Start', 'End']:
|
||||||
|
# Get raw dict
|
||||||
|
raw = file[f'{x}Time']
|
||||||
|
# Replace certain keys
|
||||||
|
for k, v in replace_fields.items():
|
||||||
|
if k in raw.keys():
|
||||||
|
raw[v] = raw.pop(k)
|
||||||
|
time_range[x.lower()] = dt(**raw)
|
||||||
|
start, end = time_range.values()
|
||||||
|
processed_motions.append({
|
||||||
|
'start': start,
|
||||||
|
'end': end,
|
||||||
|
'filename': file['name']
|
||||||
|
})
|
||||||
|
return processed_motions
|
||||||
|
|||||||
Reference in New Issue
Block a user