Skip to content

Commit 86117de

Browse files
author
Bobrock
committed
Improvements to motion detection and download methods, add to examples
1 parent 00835e3 commit 86117de

File tree

4 files changed

+107
-6
lines changed

4 files changed

+107
-6
lines changed

examples/download_motions.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import os
2+
from configparser import RawConfigParser
3+
from datetime import datetime as dt, timedelta
4+
from reolink_api import Camera
5+
6+
7+
def read_config(props_path: str) -> dict:
8+
"""Reads in a properties file into variables.
9+
10+
NB! this config file is kept out of commits with .gitignore. The structure of this file is such:
11+
# secrets.cfg
12+
[camera]
13+
ip={ip_address}
14+
username={username}
15+
password={password}
16+
"""
17+
config = RawConfigParser()
18+
assert os.path.exists(props_path), f"Path does not exist: {props_path}"
19+
config.read(props_path)
20+
return config
21+
22+
23+
# Read in your ip, username, & password
24+
# (NB! you'll likely have to create this file. See tests/test_camera.py for details on structure)
25+
config = read_config('../secrets.cfg')
26+
27+
ip = config.get('camera', 'ip')
28+
un = config.get('camera', 'username')
29+
pw = config.get('camera', 'password')
30+
31+
# Connect to camera
32+
cam = Camera(ip, un, pw)
33+
34+
start = (dt.now() - timedelta(hours=1))
35+
end = dt.now()
36+
# Collect motion events between these timestamps for substream
37+
processed_motions = cam.get_motion_files(start=start, end=end, streamtype='sub')
38+
39+
dl_dir = os.path.join(os.path.expanduser('~'), 'Downloads')
40+
for i, motion in enumerate(processed_motions):
41+
fname = motion['filename']
42+
# Download the mp4
43+
resp = cam.get_file(fname, output_path=os.path.join(dl_dir, f'motion_event_{i}.mp4'))

reolink_api/APIHandler.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import requests
12
from reolink_api.resthandle import Request
23
from .alarm import AlarmAPIMixin
34
from .device import DeviceAPIMixin
@@ -109,7 +110,23 @@ def _execute_command(self, command, data, multi=False):
109110
try:
110111
if self.token is None:
111112
raise ValueError("Login first")
112-
response = Request.post(self.url, data=data, params=params)
113+
if command == 'Download':
114+
# Special handling for downloading an mp4
115+
# Pop the filepath from data
116+
tgt_filepath = data[0].pop('filepath')
117+
# Apply the data to the params
118+
params.update(data[0])
119+
with requests.get(self.url, params=params, stream=True) as req:
120+
if req.status_code == 200:
121+
with open(tgt_filepath, 'wb') as f:
122+
f.write(req.content)
123+
return True
124+
else:
125+
print(f'Error received: {req.status_code}')
126+
return False
127+
128+
else:
129+
response = Request.post(self.url, data=data, params=params)
113130
return response.json()
114131
except Exception as e:
115132
print(f"Command {command} failed: {e}")

reolink_api/download.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
class DownloadAPIMixin:
22
"""API calls for downloading video files."""
3-
def get_file(self, filename: str) -> object:
3+
def get_file(self, filename: str, output_path: str) -> bool:
44
"""
55
Download the selected video file
66
:return: response json
@@ -9,7 +9,10 @@ def get_file(self, filename: str) -> object:
99
{
1010
"cmd": "Download",
1111
"source": filename,
12-
"output": filename
12+
"output": filename,
13+
"filepath": output_path
1314
}
1415
]
15-
return self._execute_command('Download', body)
16+
resp = self._execute_command('Download', body)
17+
18+
return resp

reolink_api/motion.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1+
from typing import Union, List, Dict
12
from datetime import datetime as dt
23

34

5+
# Type hints for input and output of the motion api response
6+
RAW_MOTION_LIST_TYPE = List[Dict[str, Union[str, int, Dict[str, str]]]]
7+
PROCESSED_MOTION_LIST_TYPE = List[Dict[str, Union[str, dt]]]
8+
9+
410
class MotionAPIMixin:
511
"""API calls for past motion alerts."""
612
def get_motion_files(self, start: dt, end: dt = dt.now(),
7-
streamtype: str = 'main') -> object:
13+
streamtype: str = 'sub') -> PROCESSED_MOTION_LIST_TYPE:
814
"""
915
Get the timestamps and filenames of motion detection events for the time range provided.
1016
@@ -38,4 +44,36 @@ def get_motion_files(self, start: dt, end: dt = dt.now(),
3844
}
3945
}
4046
body = [{"cmd": "Search", "action": 1, "param": search_params}]
41-
return self._execute_command('Search', body)
47+
48+
resp = self._execute_command('Search', body)[0]
49+
files = resp['value']['SearchResult']['File']
50+
if len(files) > 0:
51+
# Begin processing files
52+
processed_files = self._process_motion_files(files)
53+
return processed_files
54+
return []
55+
56+
@staticmethod
57+
def _process_motion_files(motion_files: RAW_MOTION_LIST_TYPE) -> PROCESSED_MOTION_LIST_TYPE:
58+
"""Processes raw list of dicts containing motion timestamps
59+
and the filename associated with them"""
60+
# Process files
61+
processed_motions = []
62+
replace_fields = {'mon': 'month', 'sec': 'second', 'min': 'minute'}
63+
for file in motion_files:
64+
time_range = {}
65+
for x in ['Start', 'End']:
66+
# Get raw dict
67+
raw = file[f'{x}Time']
68+
# Replace certain keys
69+
for k, v in replace_fields.items():
70+
if k in raw.keys():
71+
raw[v] = raw.pop(k)
72+
time_range[x.lower()] = dt(**raw)
73+
start, end = time_range.values()
74+
processed_motions.append({
75+
'start': start,
76+
'end': end,
77+
'filename': file['name']
78+
})
79+
return processed_motions

0 commit comments

Comments
 (0)