tchellomello / python-amcrest

A Python 2.7/3.x module for Amcrest and Dahua Cameras using the SDK HTTP API.
GNU General Public License v2.0
213 stars 76 forks source link

possible support for amcrest AD110 ? #137

Open aaamoeder opened 4 years ago

aaamoeder commented 4 years ago

just bought one of these and am now sad to see the doorbell isnt supported (I know I should have done better research..) any way this might be supported in the future ?

PrplHaz4 commented 4 years ago

It seems to share at least some of the API with other Amcrest cameras but is not onvif compliant. You may have better luck with specific functionality requests or maybe we can document what is not working...

aaamoeder commented 4 years ago

So basically trying to integrate this boorbell into Home assistant (which is using this code) --> https://www.home-assistant.io/integrations/amcrest/ But using that with my credentials is not working..

Just having the button push and perhaps motion detection would be AMAZING..

digiblur commented 4 years ago

@aaamoeder use the ID of admin and the device password you made during pairing.

digiblur commented 4 years ago

@tchellomello if you want I'd be glad to send you an Amcrest doorbell for you to keep and take a crack at to see what you can pull out of the configs/API on it. Let me know.

aaamoeder commented 4 years ago

@aaamoeder use the ID of admin and the device password you made during pairing.

Tried that, but sadly it's not working.

digiblur commented 4 years ago

@aaamoeder I would definitely try to hit with VLC on the RTSP stream then to verify you don't have any networking or password issues as I do know that method works.

aaamoeder commented 4 years ago

have it setup as a generic camera atm and that works fine.. weird, will test more later.

reesericci commented 4 years ago

Apparently there's no API spec for the push button, get amcrest to add it!

reesericci commented 4 years ago

image She said it, not me. Go here: https://amcrest.com/forum/feature-device-request-f31/?sid=b06b1727d00da763f3bd30fa95b20c57

MYeager1967 commented 4 years ago

I could care less about the doorbell press at the moment. I'm trying to get a reliable motion event out of the doorbell. The HA integration is fairly broken at the moment and has been for a little while. I don't need the camera (picture) part of it, or anything else they're putting into it. I just want a reliable way to read the motion event...

GaryOkie commented 4 years ago

I have been using Tasker/SmartHome to trigger both doorbell button and motion events in HA reliably and quickly. But that should not at all be necessary. Since the @pnbruckner update to python-amcrest to subscribe to events rather than poll, motion events from the HA Amcrest integration are also reliable and instantaneous. Not broken at all for me. (Besides the fact that this doorbell produces far too many unwanted events uncharacteristic for a PIR sensor).

Apparently there's no API spec for the push button, get amcrest to add it!

Yes, there is (but undocumented) - I have a working Python script that detects AD110 doorbell presses! It's not integrated into HA yet, but I'll work with anyone who wants to update the python-amcrest and HA code to incorporate the event code and toggle a new doorbell button binary sensor.

MYeager1967 commented 4 years ago

Any chance you'd forward me a copy of that script? I've been working on an Appdaemon script to handle the doorbell and it's working pretty good. I'm polling, but I'm going to start looking into the subscription method when I get time.

GaryOkie commented 4 years ago

Sure! It's just a slightly modified version of an app Phil wrote for testing subscriptions to motion events. There is some extra parsing in it that isn't necessary, but works nicely as a proof of concept. Lots of others have gone down the Appdaemon/MQTT route and custom code to handling Dahua cameras, but my hope is we can get this Amcrest code updated for full support of the doorbell as well as Dahua IVS capability and NVR's.

The key bit was discovering the undocumented doorbell button event code _DoTalkAction_. Upon a button press, this event will return "Action" : "Invite". If the doorbell is answered via the SmartHome app, the event will return : "Answer" . And when call ends or is not answered, "Hangup" is returned. The only thing actually relevant for a button sensor is simply parsing for "Invite".

from datetime import datetime
import sys

from amcrest import Http

def lines(ret):
    line = ''
    for char in ret.iter_content(decode_unicode=True):
        line = line + char
        if line.endswith('\r\n'):
            yield line.strip()
            line = ''

def main():
    if len(sys.argv) != 5:
        print(f'{sys.argv[0]} host port user password')
        sys.exit(1)

    host = sys.argv[1]
    port = sys.argv[2]
    user = sys.argv[3]
    pswd = sys.argv[4]

    cam = Http(host, port, user, pswd, retries_connection=1, timeout_protocol=3.05)

    ret = cam.command(
        'eventManager.cgi?action=attach&codes=[_DoTalkAction_]',
        timeout_cmd=(3.05, None), stream=True)
    ret.encoding = 'utf-8'

    try:
        for line in lines(ret):
            if "Invite" in line:
                print(
                    datetime.now().replace(microsecond=0),
                    ' - Doorbell Button Pressed'
                )
    except KeyboardInterrupt:
        ret.close()
        print(' Done!')

if __name__ == '__main__':
    main()

NOTE: The DoTalkAction code is SmartHome-specific. There is also a more general CallNoAnswered (Start/Stop) event that always occurs at the same time the button is pressed. I believe this event is also used by Dahua doorbells and VTO devices, so it is more suitable than DoTalkAnswer for python-amcrest implementation.

MYeager1967 commented 4 years ago

How possible would it be for this simple routine to watch for multiple events? I'm assuming it wouldn't take anything more than properly defining another 'ret' with a different name and another 'try' to check it as well. I'm currently using a little gem I found call onvif-motion-events to monitor for motion events on my cameras and start/stop recording in motion/motionEye. I'd like to be able to expand the capabilities of that program but I haven't figured out how duplicate the repo that it's in so that I can recompile it when I need to. It's in javascript which isn't much of an issue but python is easier to work with.

pnbruckner commented 4 years ago

@MYeager1967

The HA integration is fairly broken at the moment and has been for a little while.

What exactly do you believe is broken? Have you opened an issue in the HA repo?

I would be happy to consider adding support for the doorbell if Amcrest provided an API spec. But given the fact that they no longer even document the API of cameras they used to document, and other responses I've seen other people post, I'm not holding my breath.

Also, I'm not sure it would make sense to modify the HA amcrest integration to only support binary sensors and not the camera functionality, but I wouldn't necessarily rule that out if enough people thought that made sense.

GaryOkie commented 4 years ago

Phil,

Dahua apparently considers their API document confidential now, so this probably explains why Amcrest can't make their version public anymore. The latest Dahua API document ( V2.84) is available here. It requires an NDA to register for a login.

Now, that's not to say that Amcrest's API is still identical to Dahua's like it used to be. RTSP, Motion detection and Indicator Light control already works as-is for the AD110 using the existing Amcrest code. (There is a difference in toggling MD though based on my tests comparing how the SmartHome app works - it currently toggles Alarm.Enable[0] instead).

Adding a new event code "_DoTalkAction_" parsing for "Invite" in the reply signals that the doorbell button has been pressed. I didn't find this code in any documentation. I just discovered it by simply using an event code of "All" in the code above to display every event the doorbell triggers. So in a way, the code is "documented" simply by using a previously documented API call to show exactly what the events are :)

BTW - You can find a version 2.76 of the Dahua API that's only a year old on the ipcamtalk forum. Maybe see if Amcrest has an NDA for their version?

GaryOkie commented 4 years ago

How possible would it be for this simple routine to watch for multiple events? I'm assuming it wouldn't take anything more than properly defining another 'ret' with a different name and another 'try' to check it as well.recompile it when I need to. It's in javascript which isn't much of an issue but python is easier to work with.

pretty simple! As mentioned above...

'eventManager.cgi?action=attach&codes=[All]',

will show every event, including stuff you wouldn't care about, like querying an NTP server every 5 minutes and adjusting the time.

According to the API doc, you can specify multiple codes separated by commas in the same call, so you could also try:

'eventManager.cgi?action=attach&codes=[VideoMotion, CrossLineDetection]',

(I am away from home and haven't tried this myself).

This discussion about advanced IVS features that pertain to Dahua cameras and not the doorbell should be moved to a different thread. I would suggest the Dahua IP Camera Feature requests topic a good place. I had commented there a few days ago as well.

MYeager1967 commented 4 years ago

As soon as I get around to slapping this in a docker container, I'll let you know what I learn. Move it wherever you think is appropriate, just let me know so I can follow it there....

I got this stuffed into a docker container to play with and it does indeed give me WAY more info than I'll ever need with [All]. Not sure if it's working properly with multiple events to look for, will work on that. That said, I'll set one up to monitor the doorbell and see what useful information I get out of it. May be a day or two before I get that done...

digiblur commented 4 years ago

@GaryOkie I have some digging to do on the flood light camera. Uses the same app as the doorbell. I want to be able to turn the lights on and off.

GaryOkie commented 4 years ago

@digiblur - I've come up with a fairly easy way to determine exactly what configuration setting in the camera is updated by the SmartHome app.

First, turn the lights off with the app, then run this command: _/cgi-bin/configManager.cgi?action=getConfig&name=All_

this will display several thousand (!) lines of internal camera configs in your browser. Select all and copy to a file or spreadsheet.

Then turn on the lights via the app and run the command/copy/paste results again.

Diff the 2 files to see exactly what setting was changed from false to true. That setting should then be able to be changed with an API setConfig command.

(There is a free windiff utility if you don't have a linux system, or don't want to use Excel to compare cells)

digiblur commented 4 years ago

Nice! Thanks for confirming exactly what the process that I believe I need to do. I'll throw them into Notepad++ and use the compare plugin to do the diff. I appreciate it! I dug through a bit of a few SDK commands already and found some minor differences between the doorbell.

digiblur commented 4 years ago

Hmm...not seeing any difference. I can toggle the light as motion activated to kick on the floods and see that change but nothing on just turning on the lights. I'll have to do a packet capture but I have this feeling it's going out to the cloud and back.

GaryOkie commented 4 years ago

Huh, that's certainly unexpected. Every SmartHome setting I tried for the doorbell was identified in this manner. Good luck with the packet capture. I tried several and they all interfered with the ability to stream video in the app, but that shouldn't be an issue for testing the lights.

digiblur commented 4 years ago

Technically it isn't a setting, it's just an action to turn on the floodlight and/or siren on the main screen under the video stream.

digiblur commented 4 years ago

I looked at your piece above about subscribing to all codes and happened to see the status of the "WightLight" :) I could see myself turning it on and off.

Code=LeFunctionStatusSync;action=Pulse;index=0;data={ "Function" : "WightLight", "Status" : true }

I'll have to give it a shot on the doorbell for the button as that's just an event we need to read so it should work as you say.

digiblur commented 4 years ago

I do see what you were talking about with the events of pushing the bell. Interesting...it is indeed there!

Code=DoTalkAction;action=Pulse;index=0;data={ "Action" : "Invite", "CallID" : "xxxxxxxxxxxxx", "CallSrcMask" : 4 }

Saw some other stuff of course.

Code=VideoMotion;action=Start;index=0 Code=AlarmLocal;action=Start;index=6 Code=ProfileAlarmTransmit;action=Start;index=0;data={ "SenseMethod" : "PassiveInfrared", "UTC" : 1597942508 } Code=UpdateFile;action=Pulse;index=0 Code=AlarmLocal;action=Stop;index=6 Code=VideoMotion;action=Stop;index=0 Code=ProfileAlarmTransmit;action=Stop;index=0;data={ "SenseMethod" : "PassiveInfrared", "UTC" : 1597942518 }

GaryOkie commented 4 years ago

"WightLight?" LOL, A Ghost light or the developer didn't know how to spell White? Anyway, that's an interesting discovery you see this LeFunctionStatusSync event tripped.

MYeager1967 commented 4 years ago

I'm monitoring the doorbell with the [All] setting to see what I can glean from it. Also watching one of my cameras with the same routine. Learning a few things about the camera, the doorbell has been pretty boring so far. Not much going on outside and I'm not feeling like walking out there. :-) I'll have company in a bit and they always ring the bell before they come in.

digiblur commented 4 years ago

@GaryOkie maybe the dev was keeping it legit from the cancel culture bots on the net? :)

I'll have to look at some NodeRed nodes to see if I can attach and grab stuff that way. Should be simple in theory...

GaryOkie commented 4 years ago

Oh no! How am I going to illuminate all my trippy fluorescent 70's posters?

MYeager1967 commented 4 years ago

I'm watching this and noticing that it's not catching the if "WhatEver" in line:

When I try to print line to see what's in it, I'm not getting anything that resembles what I expect to see. The code in the original script is still printing out what I expect to see, but that's it. Probably something I screwed up, and I'm looking into that...

MYeager1967 commented 4 years ago

amotion2.zip

Here's what I'm working with if anyone cares to tell me why I can't see the contents of 'line'

MYeager1967 commented 4 years ago

Oh no! How am I going to illuminate all my trippy fluorescent 70's posters?

With a BlakLight.....

GaryOkie commented 4 years ago

I don't fully understand all the inner decoding the original script is doing, but that's exactly how it is done throughout the Python-amcrest code, not just this script. So whatever you took out, why not put it back in? I did try to open your zip, not that I would know really what to tell you, but got this error from Winzip: unsupported compression method 98.

MYeager1967 commented 4 years ago

That's just it, I don't think I took anything out. I modified it to get the variables from docker's environment variables, but that's really about it. The zip was a zipx (had to remove the x for it to upload, didn't figure it mattered). Pasting code does odd things to the formatting. Oh well...


from datetime import datetime
import os
import sys

from amcrest import Http

def lines(ret):
    line = ''
    for char in ret.iter_content(decode_unicode=True):
        line = line + char
        if line.endswith('\r\n'):
            yield line.strip()
            line = ''

def main():
    user = os.environ['MY_USER']
    pswd = os.environ['MY_PASS']
    host = os.environ['HOST']
    port = os.environ['PORT']

    cam = Http(host, port, user, pswd, retries_connection=1, timeout_protocol=3.05)

    ret = cam.command(
        'eventManager.cgi?action=attach&codes=[All]',
        timeout_cmd=(3.05, None), stream=True)
    ret.encoding = 'utf-8'

    try:
        for line in lines(ret):
            if "Invite" in line:
                print(
                    datetime.now().replace(microsecond=0),
                    ' - Doorbell Button Pressed'
                )
            if "VideoMotion" in line:
                print('Motion')
                if "Start" in line:
                    print(
                        datetime.now().replace(microsecond=0),
                        ' - Motion Start'
                    )
                if "Stop" in line:
                    print(
                        datetime.now().replace(microsecond=0),
                        ' - Motion Stop'
                    )
            if "CrossLineDetection" in line:
                if "Start" in line:
                    print(
                        datetime.now().replace(microsecond=0),
                        ' - Intrusion Start'
                    )
                if "Stop" in line:
                    print(
                        datetime.now().replace(microsecond=0),
                        ' - Intrusion Stop'
                    )
            if line.lower().startswith('content-length:'):
                chunk_size = int(line.split(':')[1])
                print(
                    datetime.now().replace(microsecond=0),
                    repr(next(ret.iter_content(
                        chunk_size=chunk_size, decode_unicode=True))),
                )
    except KeyboardInterrupt:
        ret.close()
        print(' Done!')

if __name__ == '__main__':
    main()
MYeager1967 commented 4 years ago

Seeing as this isn't running anywhere near an HA instance, I did a pip3 install amcrest to bring in the amcrest code. Should it have been python-amcrest? I just tried it with python-amcrest and it kicked it back, so that answers that question. Do I need this line:

eval "$(register-python-argcomplete amcrest-cli)"

GaryOkie commented 4 years ago

I don't know. I just ran the script in the HA docker for testing and had no trouble with dependencies.

MYeager1967 commented 4 years ago

I'm not having any issue with dependencies. The data is there, I think I just have to convert it before I try to test it.

MYeager1967 commented 4 years ago

Ok, I used the code in the print line to decode the response into a usable variable and everything functions as expected. I did notice that it seems to time out at different points and through connection errors. Anyone else have this experience? This didn't surprise me too much on the doorbell, but it did on a POE camera. Shouldn't have issues on a wired camera... Cleaning it up a bit and testing further...

Anyone know what AlarmLocal is for on the doorbell? It seems to actuate completely independently of the motion detection.

GaryOkie commented 4 years ago

@MYeager1967 - funny you asked about AlarmLocal. That event was the center of a discussion yesterday on the Amcrest community forum here!

What we are seeing is that the AD110 has Motion detection always enabled, even when the SmartHome app supposedly turns it off. Well, what the app is really doing is toggling Alarm[0] in the API which in turn generates AlarmLocal events. The app is just ignoring the other MD event which occur far more frequently and is only pushing notifications and capturing recordings when AlarmLocal Start occurs with either index 6 or 7. Not clear what the index refers to.

I suppose it is doing this to keep the SmartHome app's filtering of motion using it's PIR sensitivity/detection area blocks to itself. This perhaps was done to let other apps like BlueIris or even the NVR to process "raw" MD events as they see fit. That's speculation on my part at this point.

You might find the code @pcabral wrote to be useful and compare it to see if it has any timeout issues. I did not notice any timeouts with the original script, running on HA docker.

MYeager1967 commented 4 years ago

I'll have to check that discussion out. I don't disable motion detection so I'm not sure that's what's going on here but there's nothing in the app to indicate there's any kind of issue either. I'm not getting any reading on the index on the AlarmLocal and VideoMotion lines, might be cutting them off (who knows). On everything else, I get an index reading and some data. Great work you (and the original script author) have done...

GaryOkie commented 4 years ago

I had trouble with the AlarmLocal Index reading too using the original script. It is on a new line.

matchett808-gh commented 4 years ago

Just like to pop in and say thanks for working on this

I've mangled this into a Python Docker container that publishes (only) doorbell pushes to MQTT (really just modifying the code slightly) - will test for a day or two then share if useful, appears stable for the time being

MYeager1967 commented 4 years ago

I'm actually working on stuffing it all into a docker container that uses MQTT to notify HA of doorbell presses, line crossing and region crossing events (running Dahua firmware). It will also use the webcontrol interface to start and stop recording in motionEye. I use a javascript utility called onvif-motion-events to do it at the moment. I'm trying to figure out how to modify it as it works extremely well on the cameras but it doesn't work at all on the doorbell. It's an NPM install item though and I've had trouble getting it to install from a local source instead of the NPM repository.

This being python and much easier to modify, it may actually be the better route.

It seems I've managed to get the whole response now. Much better when you can actually see what is going on. I do have a question though. Is this script actually subscribing to the event channel or is it still polling on a constant basis?

MYeager1967 commented 4 years ago

Just like to pop in and say thanks for working on this

I've mangled this into a Python Docker container that publishes (only) doorbell pushes to MQTT (really just modifying the code slightly) - will test for a day or two then share if useful, appears stable for the time being

I'm assuming the code to connect to the broker goes outside of the main()? I'm just starting to add the MQTT stuff now that I have the responses figured out...

matchett808-gh commented 4 years ago

Here's the file

from datetime import datetime
import os
import re
import sys
from pprint import pprint
from amcrest import Http
import json
import paho.mqtt.client as mqtt  #import the client1
import time
Connected = False 
glob_client = None

def on_connect(client, userdata, flags, rc):
    if rc == 0:
        print("Connected to broker")
        global Connected                #Use global variable
        Connected = True                #Signal connection 
        global glob_client
        glob_client = client
    else:
        print("Connection failed")

def lines(ret):
    line = ''
    for char in ret.iter_content(decode_unicode=True):
        line = line + char
        if line.endswith('\r\n'):
            yield line.strip()
            line = ''

def parseline(line):
    if Connected == False:
        return
    code   = ''
    action = ''
    QOS = 2

    m = re.search('Code=(.*?);', line)
    if m:
        code = m.group(1)
        print(f'>>>>>>>>>>>>>> code: {code}')

    m2 = re.search('action=(.*?);', line)
    if m2:
        action = m2.group(1)
        print(f'>>>>>>>>>>>>>> action: {action}')
    m3 = re.search(';data={(.*)}', line, flags = re.S)
    if m3:
        data = m3.group(1).replace('\n', '')
        data = f'{{ {data} }}'
        obj = json.loads(data)

    if code == '_DoTalkAction_':
        print(obj['Action'])
        if obj['Action'] == 'Invite':
            glob_client.publish('doorbell/visitor','YES',QOS,False)
        elif obj['Action'] == 'Hangup':
            glob_client.publish('doorbell/visitor','NO',QOS,False)
    else:
        print(f'Code {code} ignored')

def main():
    print ('startup')
    user        = os.environ['amcrest_user']
    pswd        = os.environ['amcrest_pass']
    host        = os.environ['amcrest_host']
    mqtt_host   = os.environ['mqtt_host']
    mqtt_user   = os.environ['mqtt_user']
    mqtt_pass   = os.environ['mqtt_password']
    port        = 80

    print(f'mqtt host: {mqtt_host}')

    client = mqtt.Client(client_id='Amcrest Doorbell', clean_session=True, userdata=None, transport='tcp')
    client.username_pw_set(mqtt_user,mqtt_pass)
    client.on_connect= on_connect                      #attach function to callback

    client.connect(mqtt_host)
    client.loop_start()
    while Connected != True:    #Wait for connection
        time.sleep(0.1)

    cam = Http(host, port, user, pswd, retries_connection=1, timeout_protocol=3.05)
    ret = cam.command('eventManager.cgi?action=attach&codes=[All]',timeout_cmd=(3.05, None), stream=True)
    ret.encoding = 'utf-8'

    try:
        for line in lines(ret):
            parseline(line)
    except KeyboardInterrupt:
        ret.close()
        print(' Done!')
        client.disconnect()
        client.loop_stop()

if __name__ == '__main__':
    main()

I just spent little this afternoon banging it together from your code an an MQTT tut - so there are various style issues but it's functional - altering the parse function you could add motion quite easily

Additionally, the static topic isn't very good haha - would also prefer autodiscovery, topic config, will messages etc

dockerfile:

FROM python:3.8

WORKDIR /usr/src/app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD [ "python","-u", "./main.py" ]

requirements file looks like:

amcrest==1.7.1
paho-mqtt==1.5.0

and a env.dist:

amcrest_user=
amcrest_pass=
amcrest_host=
mqtt_host=
mqtt_user=
mqtt_password=

Then it's used in docker compose like so:

  doorbell:
    restart: always
    build:
      context: ./amcrest
    env_file:
      - ./amcrest/env
matchett808-gh commented 4 years ago

If you're putting it in Docker, pay close attention to the -u in the docker file

MYeager1967 commented 4 years ago

Your code is actually pretty similar to mine, but I didn't put the MQTT connection code inside the main(). Probably doesn't make that much difference, but I like your idea for parsing. I hadn't gotten that far yet and, so far, was just using the if x in y method. Seeing as I'm looking to flush this out into a full solution, your method may be much better. A dynamic topic isn't difficult. Not sure what the purpose of the keyboard interupt is with it being in docker. Probably need to intercept sigterm or something similar.

What's the purpose of the -u? Right now I'm using it without it. I'm just building from a dockerfile if that makes a difference. I set my ENV variables from the run command as I'm running several instances of this, one for each camera when I'm done.

MYeager1967 commented 4 years ago

Is it just me, or does the VideoMotion code toggle constantly on this doorbell even though there's really nothing going on? I understand now why the other code is looking at the AlarmLocal code rather than VideoMotion. AlarmLocal, action=start,index=7 seems to tell the app to sound the chime that there's someone out there. It even triggered the doorbell camera itself to record. The polling method I'm using at the moment to catch these events didn't catch it. This will be a far better method of processing the events from these cameras...

GaryOkie commented 4 years ago

That's what I was saying earlier - VideoMotion (via MotionDetect config setting) is ALWAYS enabled. What motion actually triggers VideoMotion as opposed to AlarmLocal is as yet unclear, but what we do know is that they occur more frequently.

This suggests that VideoMotion events are not being filtered by the PIR sensitivity/range/detection blocks, or even using the PIR sensor at all, and maybe just represents pixel changes to trigger movement? Again, this is not clear.

However, I do know that my Dahua NVR which records live streams from the doorbell also keeps a log of events in the Alarm section, and it records far more events than the SmartHome app registers. The reasoning for this could be to allow AI NVR's (or BlueIris, MotionEye, etc.) to analyze the motion events independently of SmartHome filtering.