I have a lot of security cameras, several analog cameras connected to a Dahua DVR unit and more Amcrest IP Cameras stand alone (though these do work with the DVR).

I wanted to be able to use the netcam plugin and HS3's own camera support with these devices but apparently neither can access them due to some security constraints.

To get around this issue, I did not want to remove password control on my camera (not sure it could even be completely done) yet eliminate the access blocking that it did to HS3.

That's when I discovered the Amcrest HTTP API and the Dahua Dahua IPC HTTP API as well as the Amcrest Python library.

Amcrest+HTTP+API+3.2017.pdf

DAHUA_IPC_HTTP_API_V1.00x.pdf

Amcrest Python Library

This got me to thinking about creating a small proxy server that could access the cameras via the python library and return images or execute PTZ movements on the behalf of HS3.

So I coded up a small python multi-threaded HTTP server that would allow HS3 to interface with the cameras. I'm new to Python so this was taken from examples on the web, and executes on my Linux based HS3 server running on Ubuntu Bionic Beaver 18.04.1

WARNING -- this eliminates all password protection controls for anything going through the proxy so I limit access to the IP Link Local address 127.0.0.1 and run it on the HS3 server.

Here is the Python Script:

Code:
#!/usr/bin/python
import sys
import re
import time, threading, socket, SocketServer, BaseHTTPServer


from BaseHTTPServer import BaseHTTPRequestHandler,HTTPServer
from ConfigParser import ConfigParser, NoOptionError, NoSectionError
from os import curdir, sep, path, getenv
from amcrest import AmcrestCamera
from urlparse import parse_qs, urlparse


# create requests in the format of the example URL below -- there is no security or authentication used so best to serve only on localhost address!!!
# http://{host{:{port}/?camera={camera}&channel=n&command={snap,ptz}?direction={Up,Down,Left,Right}?action={start,stop}


ADDR_NUMBER = "127.0.0.1"
PORT_NUMBER = 8087
#AMCREST_CONF = path.join(getenv("HOME"), '.config/amcrest.conf')
AMCREST_CONF = path.join('/opt/HomeSeer/', 'amcrest.conf')
MAX_THREADS = 22




def get_query_field(url, field):
    try:
        return parse_qs(urlparse(url).query)[field]
    except KeyError:
        return [""]

def expand_ranges(s):
    return re.sub(
        r'(\d+)-(\d+)',
        lambda match: ','.join(
            str(i) for i in range(
                int(match.group(1)),
                int(match.group(2)) + 1
            )
        ),
        s
    )

class Camera:
    def __init__(self, cameraobj, channelst, ptzallowed, ptzchnlst):
        self.cameraobj = cameraobj
        self.channelst = channelst
        self.ptzallowed = ptzallowed
        self.ptzchannelst = ptzchnlst

camlist = dict()

if path.isfile(AMCREST_CONF):
    try:
        config = ConfigParser({'channels':'1', 'ptzcapable':'False', 'ptzchannels':'1', 'port':'80'})
        config.read(AMCREST_CONF)
    except:
        print("ERROR! opening configuration file")
        sys.exit(-1)


    for section in config.sections():
        try:
            hostname = config.get(section, 'hostname')
            port = config.getint(section, 'port')
            channels = expand_ranges(config.get(section, 'channels'))
            ptzops = config.getboolean(section,'ptzcapable')
            ptzchns = expand_ranges(config.get(section, 'ptzchannels'))
            username = config.get(section, 'username')
            password = config.get(section, 'password', raw=True)
        except (NoSectionError, NoOptionError) as e:
            print("ERROR! %s found at %s" % (e, AMCREST_CONF))
            sys.exit(-1)

        try:
            camlist[section]=Camera(AmcrestCamera(hostname,port,username,password).camera, channels.split(','), ptzops, ptzchns.split(','))
        except (TypeError, ValueError, AttributeError) as e:
            print("ERROR! %s creating camera object for %s" % (e, section))
            sys.exit(-1)
        except:
            print("ERROR! unknown problem creating camera object for %s" % (section))
            sys.exit(-1)

else:
            print("ERROR! configuration file not found")
            sys.exit(-1)



#This class will handles any incoming request from
#the browser
class myHandler(BaseHTTPRequestHandler):

        #Handler for the GET requests
        def do_GET(self):
                if self.path=="/":
                        self.send_error(404,'no camera specfied: %s' % self.path)
                        return

                camNam=get_query_field(self.path,"camera")[0]
                if camNam not in camlist:
                        self.send_error(404,'invalid camera specfied: %s' % self.path)
                        return

                camNum=get_query_field(self.path,"channel")[0]
                if camNum not in camlist[camNam].channelst:
                        self.send_error(404,'invalid channel specfied: %s' % self.path)
                        return

                camCmd=get_query_field(self.path,"command")[0]
                if camCmd not in ['snap','ptz']:
                        self.send_error(404,'invalid command specfied: %s' % self.path)
                        return

                if camCmd == "ptz" and camlist[camNam].ptzallowed == False:
                        self.send_error(404,'camera not pan-tilt-zoom capable: %s' % self.path)
                        return


                if camCmd == "ptz" and camlist[camNam].ptzallowed == True:
                    camDir=get_query_field(self.path,"direction")[0]
                    if camDir not in ['Up','Down','Left','Right']:
                            self.send_error(404,'invalid direction specfied: %s' % self.path)
                            return

                    camAct=get_query_field(self.path,"action")[0]
                    if camAct not in ['stop','start']:
                            self.send_error(404,'invalid action specfied: %s' % self.path)
                            return

                    if camNum not in camlist[camNam].ptzchannelst:
                            self.send_error(404,'invalid channel for ptz operations specfied: %s' % self.path)
                            return


                try:
                        if camCmd == "snap":
                                self.send_response(200)
                                self.send_header('Content-type','image/jpg')
                                self.end_headers()
                                self.wfile.write(camlist[camNam].cameraobj.snapshot(camNum).read())

                        elif camCmd == "ptz":
                                self.send_response(200)
                                self.end_headers()
                                camlist[camNam].cameraobj.ptz_control_command(channel=camNum, action=camAct, code=camDir, arg1=None, arg2=1, arg3=None)
                        return

                except IOError as err:
                            print("IO error: {0}".format(err))
                            self.send_error(503,'Service Unavailable: %s' % self.path)


# Create ONE socket.
addr = (ADDR_NUMBER, PORT_NUMBER)
sock = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(addr)
sock.listen(5)

# Launch 100 listener threads.
class Thread(threading.Thread):
    def __init__(self, i):
        threading.Thread.__init__(self)
        self.i = i
        self.daemon = True
        self.start()
    def run(self):
        httpd = BaseHTTPServer.HTTPServer(addr, myHandler, False)
        print 'Started httpserver on ', ADDR_NUMBER, ' port ' , PORT_NUMBER

        # Prevent the HTTP server from re-binding every handler.
        # https://stackoverflow.com/questions/46210672/
        httpd.socket = sock
        httpd.server_bind = self.server_close = lambda self: None

        httpd.serve_forever()
[Thread(i) for i in range(MAX_THREADS)]
time.sleep(9e9)
Here is an example config file:
Code:
[DEFAULT]
username: USER
password: PASSWORD
port: 80

[camera1]
hostname: 192.168.1.251
username: DVRUSER
password: DVRPASSWORD
channels: 1-16
ptzcapable: True
ptzchannels: 10

[camera17]
hostname: 192.168.1.231
ptzcapable: true

[camera18]
hostname: 192.168.1.232
ptzcapable: true

#[camera19]
#hostname: 192.168.1.45
#ptzcapable: true

[camera20]
hostname: 192.168.1.47
ptzcapable: true

[camera21]
hostname: 192.168.1.96
ptzcapable: true

#[camera22]
#hostname: 192.168.1.100
#ptzcapable: true
Here is the systemd service unit file that I am using:

Code:
[Unit]
Description=Amcrest Snapshot Camera Server
Documentation=http://www.amcrest.com
After=network.target

[Service]
Type=simple
WorkingDirectory=/opt/HomeSeer
ExecStart=/usr/bin/python /opt/HomeSeer/amcrest-threaded-server.py
StandardOutput=null
#Restart=on-failure
Restart=no

[Install]
WantedBy=multi-user.target
This has been tested on the following cameras:

IP3M-941B
Software Version 2.520.AC00.18.R, Build Date: 2017-06-29
WEB Version 3.2.1.453504
ONVIF Version 2.42(V2.2.2.428697)

IP2M-841B
Software Version 2.520.AC00.18.R, Build Date: 2017-06-29
WEB Version 3.2.1.453504
ONVIF Version 2.42(V2.2.2.428697)

Device Type: Dahua X24A3L
Record Channel: 24
Alarm In: 16
Alarm Out: 6
Hardware Version: V1.0
Web: 3.2.7.84189
Onvif Server Version: 2.42(V1.1.2.444854)
Onvif Client Version: 2.4.1
System Version: 3.218.0000001.2, Build Date: 2017-08-08


Hopefully this will help anyone that wants to get their cameras working with HS3 but has trouble due to them requiring digest authentication and disabling basic URL authentication.

Feel free to use share or modify in any way you like.

Best Regards,

Mitch