Announcement

Collapse
No announcement yet.

Using Dahua and Amcrest cameras with Homeseer and HSTouch

Collapse
X
  • Filter
  • Time
  • Show
Clear All
new posts

  • Using Dahua and Amcrest cameras with Homeseer and HSTouch

    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
Working...
X