Skip to content

Refactor package to be imported as a single package #37

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Dec 19, 2020
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
secrets.cfg
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
52 changes: 42 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,37 @@
<h1 align="center"> Reolink Python Api Client </h1>

<p align="center">
<img alt="Reolink Approval" src="https://img.shields.io/badge/reolink-approved-blue?style=flat-square">
<img alt="GitHub" src="https://img.shields.io/github/license/ReolinkCameraApi/reolink-python-api?style=flat-square">
<img alt="GitHub tag (latest SemVer)" src="https://img.shields.io/github/v/tag/ReolinkCameraApi/reolink-python-api?style=flat-square">
<img alt="PyPI" src="https://img.shields.io/pypi/v/reolink-api?style=flat-square">
<img alt="Discord" src="https://img.shields.io/discord/773257004911034389?style=flat-square">
</p>

---

A Reolink Camera client written in Python.
A Reolink Camera client written in Python. This repository's purpose **(with Reolink's full support)** is to deliver a complete API for the Reolink Cameras,
although they have a basic API document - it does not satisfy the need for extensive camera communication.

Check out our documentation for more information on how to use the software at [https://reolink.oleaintueri.com](https://reolink.oleaintueri.com)


Other Supported Languages:
- Go: [reolink-go-api](https://github.com/ReolinkCameraAPI/reolink-go-api)
- Go: [reolinkapigo](https://github.com/ReolinkCameraAPI/reolinkapigo)

### Join us on Discord

https://discord.gg/8z3fdAmZJP


### Purpose

This repository's purpose is to deliver a complete API for the Reolink Camera's, ( TESTED on RLC-411WS )

### Sponsorship

### But Reolink gives an API in their documentation
<a href="https://oleaintueri.com"><img src="https://oleaintueri.com/images/oliv.svg" width="60px"/><img width="200px" style="padding-bottom: 10px" src="https://oleaintueri.com/images/oleaintueri.svg"/></a>

Not really. They only deliver a really basic API to retrieve Image data and Video data.
[Oleaintueri](https://oleaintueri.com) is sponsoring the development and maintenance of these projects within their organisation.

### How?

You can get the Restful API calls by looking through the HTTP Requests made the camera web console. I use Google Chrome developer mode (ctr + shift + i) -> Network.
---

### Get started

Expand All @@ -42,6 +45,10 @@ Install the package via Pip

pip install reolink-api==0.0.5

Install from GitHub

pip install git+https://github.com/ReolinkCameraAPI/reolink-python-api.git

## Contributors

---
Expand All @@ -50,6 +57,20 @@ Install the package via Pip

This project intends to stick with [PEP8](https://www.python.org/dev/peps/pep-0008/)

### How can I become a contributor?

#### Step 1

Get the Restful API calls by looking through the HTTP Requests made in the camera's web UI. I use Google Chrome developer mode (ctr + shift + i) -> Network.

#### Step 2

Fork the repository and make your changes.

#### Step 3

Make a pull request.

### API Requests Implementation Plan:

Stream:
Expand Down Expand Up @@ -112,3 +133,14 @@ SET:
- [x] Focus
- [X] Image (Brightness, Contrast, Saturation, Hue, Sharp, Mirror, Rotate)
- [X] Advanced Image (Anti-flicker, Exposure, White Balance, DayNight, Backlight, LED light, 3D-NR)

### Supported Cameras

Any Reolink camera that has a web UI should work. The other's requiring special Reolink clients
do not work and is not supported here.

- RLC-411WS
- RLC-423
- RLC-420-5MP
- RLC-410-5MP
- RLC-520
4 changes: 0 additions & 4 deletions api/__init__.py

This file was deleted.

44 changes: 44 additions & 0 deletions examples/download_motions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Downloads all motion events from camera from the past hour."""
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'))
3 changes: 1 addition & 2 deletions examples/streaming_video.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import cv2

from Camera import Camera
from reolink_api import Camera


def non_blocking():
Expand Down
4 changes: 4 additions & 0 deletions reolink_api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .api_handler import APIHandler
from .camera import Camera

__version__ = "0.1.2"
5 changes: 4 additions & 1 deletion api/alarm.py → reolink_api/alarm.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from typing import Dict


class AlarmAPIMixin:
"""API calls for getting device alarm information."""

def get_alarm_motion(self) -> object:
def get_alarm_motion(self) -> Dict:
"""
Gets the device alarm motion
See examples/response/GetAlarmMotion.json for example response data.
Expand Down
72 changes: 49 additions & 23 deletions api/APIHandler.py → reolink_api/api_handler.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
from .recording import RecordingAPIMixin
from .zoom import ZoomAPIMixin
from .device import DeviceAPIMixin
from .display import DisplayAPIMixin
from .network import NetworkAPIMixin
from .system import SystemAPIMixin
from .user import UserAPIMixin
from .ptz import PtzAPIMixin
from .alarm import AlarmAPIMixin
from .image import ImageAPIMixin
from resthandle import Request
import requests
from typing import Dict, List, Optional, Union
from reolink_api.alarm import AlarmAPIMixin
from reolink_api.device import DeviceAPIMixin
from reolink_api.display import DisplayAPIMixin
from reolink_api.download import DownloadAPIMixin
from reolink_api.image import ImageAPIMixin
from reolink_api.motion import MotionAPIMixin
from reolink_api.network import NetworkAPIMixin
from reolink_api.ptz import PtzAPIMixin
from reolink_api.recording import RecordingAPIMixin
from reolink_api.resthandle import Request
from reolink_api.system import SystemAPIMixin
from reolink_api.user import UserAPIMixin
from reolink_api.zoom import ZoomAPIMixin


class APIHandler(SystemAPIMixin,
NetworkAPIMixin,
UserAPIMixin,
class APIHandler(AlarmAPIMixin,
DeviceAPIMixin,
DisplayAPIMixin,
RecordingAPIMixin,
ZoomAPIMixin,
DownloadAPIMixin,
ImageAPIMixin,
MotionAPIMixin,
NetworkAPIMixin,
PtzAPIMixin,
AlarmAPIMixin,
ImageAPIMixin):
RecordingAPIMixin,
SystemAPIMixin,
UserAPIMixin,
ZoomAPIMixin):
"""
The APIHandler class is the backend part of the API, the actual API calls
are implemented in Mixins.
Expand All @@ -30,7 +36,7 @@ class APIHandler(SystemAPIMixin,
All Code will try to follow the PEP 8 standard as described here: https://www.python.org/dev/peps/pep-0008/
"""

def __init__(self, ip: str, username: str, password: str, https=False, **kwargs):
def __init__(self, ip: str, username: str, password: str, https: bool = False, **kwargs):
"""
Initialise the Camera API Handler (maps api calls into python)
:param ip:
Expand Down Expand Up @@ -69,7 +75,10 @@ def login(self) -> bool:
print(self.token)
return False
else:
print("Failed to login\nStatus Code:", response.status_code)
# TODO: Verify this change w/ owner. Delete old code if acceptable.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pointing out this change here.

# A this point, response is NoneType. There won't be a status code property.
# print("Failed to login\nStatus Code:", response.status_code)
print("Failed to login\nResponse was null.")
return False
except Exception as e:
print("Error Login\n", e)
Expand All @@ -89,7 +98,8 @@ def logout(self) -> bool:
print("Error Logout\n", e)
return False

def _execute_command(self, command, data, multi=False):
def _execute_command(self, command: str, data: List[Dict], multi: bool = False) -> \
Optional[Union[Dict, bool]]:
"""
Send a POST request to the IP camera with given data.
:param command: name of the command to send
Expand All @@ -105,8 +115,24 @@ def _execute_command(self, command, data, multi=False):
try:
if self.token is None:
raise ValueError("Login first")
response = Request.post(self.url, data=data, params=params)
return response.json()
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()
except Exception as e:
print(f"Command {command} failed: {e}")
raise
4 changes: 2 additions & 2 deletions Camera.py → reolink_api/camera.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from api import APIHandler
from .api_handler import APIHandler


class Camera(APIHandler):

def __init__(self, ip, username="admin", password="", https=False):
def __init__(self, ip: str, username: str = "admin", password: str = "", https: bool = False):
"""
Initialise the Camera object by passing the ip address.
The default details {"username":"admin", "password":""} will be used if nothing passed
Expand Down
4 changes: 2 additions & 2 deletions ConfigHandler.py → reolink_api/config_handler.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import io

import yaml
from typing import Optional, Dict


class ConfigHandler:
camera_settings = {}

@staticmethod
def load() -> yaml or None:
def load() -> Optional[Dict]:
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noting here that Optional[] essentially is the same as or None

try:
stream = io.open("config.yml", 'r', encoding='utf8')
data = yaml.safe_load(stream)
Expand Down
9 changes: 8 additions & 1 deletion api/device.py → reolink_api/device.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
from typing import List


class DeviceAPIMixin:
"""API calls for getting device information."""
DEFAULT_HDD_ID = [0]

def get_hdd_info(self) -> object:
"""
Gets all HDD and SD card information from Camera
Expand All @@ -9,12 +14,14 @@ def get_hdd_info(self) -> object:
body = [{"cmd": "GetHddInfo", "action": 0, "param": {}}]
return self._execute_command('GetHddInfo', body)

def format_hdd(self, hdd_id: [int] = [0]) -> bool:
def format_hdd(self, hdd_id: List[int] = None) -> bool:
"""
Format specified HDD/SD cards with their id's
:param hdd_id: List of id's specified by the camera with get_hdd_info api. Default is 0 (SD card)
:return: bool
"""
if hdd_id is None:
hdd_id = self.DEFAULT_HDD_ID
body = [{"cmd": "Format", "action": 0, "param": {"HddInfo": {"id": hdd_id}}}]
r_data = self._execute_command('Format', body)[0]
if r_data["value"]["rspCode"] == 200:
Expand Down
Loading