Skip to content
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

Adding camera discovery utilities #38

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Implement a "Camera" object by passing it an IP address, Username and Password.

See the `examples` directory.

### Using the library as a Python Module
### Installation

Install the package via PyPi

Expand All @@ -49,6 +49,48 @@ Install from GitHub

pip install git+https://github.com/ReolinkCameraAPI/reolinkapipy.git

### Usage

```python

import reolinkapi

if __name__ == "__main__":
# this will immediately log in with default camera credentials
cam = reolinkapi.Camera("192.168.0.100")

# OR in the case of managing a pool of cameras' - it's better to defer login
# foo is the username
# bar is the password
_ = reolinkapi.Camera("192.168.0.100", "foo", "bar", defer_login = True)

# to scan your network for reolink cameras

# when UPnP is enabled on the camera, simply use:
discovery = reolinkapi.Discover()
devices = discovery.discover_upnp()

# OR
_ = discovery.discover_port()

# when many cameras share the same credentials
# foo is the username
# bar is the password
factory = reolinkapi.CameraFactory(username="foo", password="bar")
_ = factory.get_cameras_from_devices(devices)

# when using the CameraFactory, we need to log in manually on each camera
# since it creates a pool of cameras
# one can use the utility function in camera factory to run an asyncio task
factory.initialise_cameras()

# now one can check if the camera has been initialised
for camera in factory.cameras:
if camera.is_loggedin():
print(f'Camera {camera.ip} is logged in')
else:
print(f'Camera {camera.ip} is NOT logged in')
```
## Contributors

---
Expand Down
28 changes: 28 additions & 0 deletions examples/basic_usage.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,39 @@
import reolinkapi

if __name__ == "__main__":
# create a single camera
# defer_login is optional, if nothing is passed it will attempt to log in.
cam = reolinkapi.Camera("192.168.0.102", defer_login=True)

# must first login since I defer have deferred the login process
cam.login()

# can now use the camera api
dst = cam.get_dst()
ok = cam.add_user("foo", "bar", "admin")
alarm = cam.get_alarm_motion()

# discover cameras on the network
discovery = reolinkapi.Discover()

# can use upnp
devices = discovery.discover_upnp()

# or port scanning using default ports (can set this when setting up Discover object
devices = discovery.discover_port()

# create a camera factory, these will authenticate every camera with the same username and password
factory = reolinkapi.CameraFactory(username="foo", password="bar")

# create your camera factory, can immediately return the cameras or just keep it inside the factory
_ = factory.get_cameras_from_devices(devices=devices)

# initialise the cameras (log them in)
factory.initialise_cameras()

# one can check if the camera is authenticated with it's `is_loggedin` property
for camera in factory.cameras:
if camera.is_loggedin:
print(f'camera {camera.ip} is logged in')
else:
print(f'camera {camera.ip} is not logged in')
2 changes: 2 additions & 0 deletions reolinkapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from reolinkapi.handlers.api_handler import APIHandler
from reolinkapi.utils.camera_factory import CameraFactory
from reolinkapi.utils.discover import Discover
from .camera import Camera

__version__ = "0.1.2"
5 changes: 5 additions & 0 deletions reolinkapi/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,8 @@ def __init__(self, ip: str,

if not defer_login:
super().login()

@property
def is_loggedin(self):
return self.token is not None

2 changes: 1 addition & 1 deletion reolinkapi/mixins/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import requests
from PIL.Image import Image, open as open_image

from reolinkapi.utils.rtsp_client import RtspClient
from reolinkapi.utils import RtspClient


class StreamAPIMixin:
Expand Down
3 changes: 3 additions & 0 deletions reolinkapi/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .discover import Discover
from .rtsp_client import RtspClient
from .camera_factory import CameraFactory
44 changes: 44 additions & 0 deletions reolinkapi/utils/camera_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import asyncio
from typing import List

from pyphorus.devices import Device

from reolinkapi import Camera


class CameraFactory:

def __init__(self, username="admin", password=""):
self._username = username
self._password = password
self._cameras = []

@property
def cameras(self):
return self._cameras

def get_cameras_from_devices(self, devices: List[Device]) -> List[Camera]:
# only get the ip from each device

ips = []
for device in devices:
ips.append(device.ip)

for ip in ips:
self._cameras.append(Camera(ip, self._username, self._password, defer_login=True))

return self._cameras

def initialise_cameras(self, timeout: int = 2):
if len(self._cameras) == 0:
raise Exception("there are no cameras in camera factory")

async def _initialise_camera():
tasks = []
for camera in self._cameras:
tasks.append(asyncio.wait_for(camera.login(), timeout=timeout))

await asyncio.gather(*tasks)

loop = asyncio.get_event_loop()
loop.run_until_complete(_initialise_camera())
116 changes: 116 additions & 0 deletions reolinkapi/utils/discover.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import socket
from typing import List

import netaddr
import pyphorus
from pyphorus import Device


class Discover:

def __init__(self, media_port: int = 9000, onvif_port: int = 8000, rtsp_port: int = 554, rtmp_port: int = 1935,
https_port: int = 443, http_port: int = 80, unique_device_ip: bool = True):
"""
Create a discover object. Pass custom port values to override the standard ones.
:param media_port:
:param onvif_port:
:param rtsp_port:
:param rtmp_port:
:param https_port:
:param http_port:
:param unique_device_ip: strip duplicate ips (basically ignoring the ports)
"""
self._unique_device_ip = unique_device_ip
self._media_port = media_port
self._onvif_port = onvif_port
self._rtsp_port = rtsp_port
self._rtmp_port = rtmp_port
self._https_port = https_port
self._http_port = http_port

self._phorus = pyphorus.Pyphorus()

self._devices = []

@property
def media_port(self):
return self._media_port

@property
def onvif_port(self):
return self._onvif_port

@property
def rtsp_port(self):
return self._rtsp_port

@property
def rtmp_port(self):
return self._rtmp_port

@property
def https_port(self):
return self._https_port

@property
def http_port(self):
return self._http_port

def list_standard_ports(self):
return {
"http": self._http_port,
"https": self._https_port,
"media": self._media_port,
"onvif": self._onvif_port,
"rtsp": self._rtsp_port,
"rtmp": self._rtmp_port,
}

def discover_upnp(self) -> List[Device]:
"""
discover cameras using UPnP
this will only work if the camera has it enabled
:return:
"""
# TODO: unsure about the reolink upnp `st`
self._devices = self._phorus.scan_upnp("upnp:reolink")

if self._unique_device_ip:
self._devices = pyphorus.utils.strip_duplicate_ips(devices=self._devices)

return self._devices

def discover_port(self, custom_cidr: str = None, additional_ports: List[int] = None) -> List[Device]:
"""
discover devices by scanning the network for open ports
this method will attempt at using the current machines' ip address to scan for open ports, to change
the ip address, change the custom_cidr field value
:param custom_cidr: cidr ip e.g. 192.168.0.0/24
:param additional_ports: a list of additional ports to add [ 9000, 100000, ... ] to the standard or overridden
ports
:return:
"""

if custom_cidr is None:
# attempt to get this machine's local ip
hostname = socket.gethostname()
ip_address = socket.gethostbyname(hostname)
# get the cidr from the ip address
cidr = netaddr.cidr_merge([ip_address])
if len(cidr) > 0:
cidr = cidr[0]
custom_cidr = cidr

custom_ports = [self._media_port, self._rtmp_port, self._rtsp_port, self._onvif_port, self._https_port,
self._http_port]

if additional_ports is not None:
# use the cameras' standard ports
custom_ports += additional_ports

self._devices = self._phorus.scan_ports(custom_cidr, ports=custom_ports)

if self._unique_device_ip:
self._devices = pyphorus.utils.strip_duplicate_ips(devices=self._devices)

return self._devices
4 changes: 3 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ def find_version(*file_paths):
'Pillow==8.0.1',
'PySocks==1.7.1',
'PyYaml==5.3.1',
'requests>=2.18.4',
'requests>=2.25.1',
'pyphorus==0.0.3',
'netaddr>=0.8.0',
]


Expand Down