dchesterton / amcrest2mqtt

Expose all events from an Amcrest device to an MQTT broker
https://hub.docker.com/r/dchesterton/amcrest2mqtt
MIT License
150 stars 35 forks source link

AD410 motion event code #6

Closed iankaufmann closed 3 years ago

iankaufmann commented 3 years ago

Hello,

I just got this hooked up today with my new AD410. Everything other than the "motion" event is working great!

I noticed that line 255 of amcrest2mqtt.py is looking for the event "ProfileAlarmTransmit".

However, I'm not seeing that in my logs anywhere.

When there is a motion event, it looks like the AD410 is using "VideoMotion" instead. I also see "AlarmLocal"... both issue a "start" and "stop" event.

08/05/2021 19:03:36 [INFO] Fetching storage sensors...

08/05/2021 19:04:28 [INFO] {'Code': 'TimeChange', 'action': 'Pulse', 'index': '0', 'data': {'BeforeModifyTime': '2021-05-08 14:04:28', 'ModifiedTime': '2021-05-08 14:04:28'}}

08/05/2021 19:04:28 [INFO] {'Code': 'NTPAdjustTime', 'action': 'Pulse', 'index': '0', 'data': {'Address': '200.160.0.8', 'Before': '2021-05-08 14:04:27', 'result': 'true'}}

08/05/2021 19:04:36 [INFO] Fetching storage sensors...

08/05/2021 19:05:23 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:05:23 [INFO] {'Code': 'VideoMotion', 'action': 'Start', 'index': '0', 'data': {'Id': '[', 'RegionName': '['}}

08/05/2021 19:05:24 [INFO] {'Code': 'StorageChange', 'action': 'Pulse', 'index': '0', 'data': {'Group': 'ReadWrite', 'Path': '\\/mnt\\/sd'}}

08/05/2021 19:05:24 [INFO] {'Code': 'StorageChange', 'action': 'Pulse', 'index': '0', 'data': {'Group': 'ReadWrite', 'Path': '\\/mnt\\/sd'}}

08/05/2021 19:05:24 [INFO] {'Code': 'StorageChange', 'action': 'Pulse', 'index': '0', 'data': {'Group': 'ReadWrite', 'Path': '\\/mnt\\/sd'}}

08/05/2021 19:05:26 [INFO] {'Code': 'StorageChange', 'action': 'Pulse', 'index': '0', 'data': {'Group': 'ReadWrite', 'Path': '\\/mnt\\/sd'}}

08/05/2021 19:05:26 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:05:27 [INFO] {'Code': 'NewFile', 'action': 'Pulse', 'index': '0', 'data': {'File': '\\/mnt\\/sd\\/2021-05-08\\/001\\/jpg\\/14\\/05\\/27[M][0@0][0].jpg', 'Size': '47488,', 'StoragePoint': 'NULL'}}

08/05/2021 19:05:28 [INFO] {'Code': 'NewFile', 'action': 'Pulse', 'index': '0', 'data': {'File': '\\/mnt\\/sd\\/2021-05-08\\/001\\/jpg\\/14\\/05\\/28[M][0@0][0].jpg', 'Size': '47273,', 'StoragePoint': 'NULL'}}

08/05/2021 19:05:28 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:05:29 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:05:29 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:05:29 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:05:29 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:05:30 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:05:36 [INFO] {'Code': 'NewFile', 'action': 'Pulse', 'index': '0', 'data': {'File': '\\/mnt\\/sd\\/2021-05-05\\/001\\/dav\\/19\\/19.21.00-19.21.00[R][0@0][0].mp4_', 'Size': '117159593,', 'StoragePoint': 'NULL'}}

08/05/2021 19:05:36 [INFO] Fetching storage sensors...

08/05/2021 19:05:40 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:05:45 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:05:46 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:05:46 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:05:48 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:05:48 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:05:48 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:05:49 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:01 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:01 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:03 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:05 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:05 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:06 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:08 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:09 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:11 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:11 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:18 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:18 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:20 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:21 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:21 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:23 [INFO] {'Code': '_CallRemoveMask', 'action': 'Start'}

08/05/2021 19:06:23 [INFO] {'Code': 'AlarmLocal', 'action': 'Start'}

08/05/2021 19:06:23 [INFO] {'Code': '_DoTalkAction_', 'action': 'Pulse', 'index': '0', 'data': {'Action': 'Invite', 'CallID': '20210508140623@608006@192.168.10.53', 'CallSrcMask': '4'}}

08/05/2021 19:06:23 [INFO] {'Code': 'CallNoAnswered', 'action': 'Start', 'index': '0', 'data': {'CallID': '2'}}

08/05/2021 19:06:23 [INFO] {'Code': 'PhoneCallDetect', 'action': 'Start'}

08/05/2021 19:06:23 [INFO] {'Code': 'StorageChange', 'action': 'Pulse', 'index': '0', 'data': {'Group': 'ReadWrite', 'Path': '\\/mnt\\/sd'}}

08/05/2021 19:06:23 [INFO] {'Code': 'StorageChange', 'action': 'Pulse', 'index': '0', 'data': {'Group': 'ReadWrite', 'Path': '\\/mnt\\/sd'}}

08/05/2021 19:06:23 [INFO] {'Code': 'StorageChange', 'action': 'Pulse', 'index': '0', 'data': {'Group': 'ReadWrite', 'Path': '\\/mnt\\/sd'}}

08/05/2021 19:06:24 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:25 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:25 [INFO] {'Code': 'StorageChange', 'action': 'Pulse', 'index': '0', 'data': {'Group': 'ReadWrite', 'Path': '\\/mnt\\/sd'}}

08/05/2021 19:06:25 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:26 [INFO] {'Code': 'NewFile', 'action': 'Pulse', 'index': '0', 'data': {'File': '\\/mnt\\/sd\\/2021-05-08\\/001\\/jpg\\/14\\/06\\/26[M][0@0][0].jpg', 'Size': '46808,', 'StoragePoint': 'NULL'}}

08/05/2021 19:06:27 [INFO] {'Code': 'NewFile', 'action': 'Pulse', 'index': '0', 'data': {'File': '\\/mnt\\/sd\\/2021-05-08\\/001\\/jpg\\/14\\/06\\/27[M][0@0][0].jpg', 'Size': '46769,', 'StoragePoint': 'NULL'}}

08/05/2021 19:06:27 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:27 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:28 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:28 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:33 [INFO] {'Code': '_DoTalkAction_', 'action': 'Pulse', 'index': '0', 'data': {'Action': 'Hangup', 'CallID': '20210508140623@608006@192.168.10.53', 'CallSrcMask': '4,', 'HangupReason': 'HangupByPhone'}}

08/05/2021 19:06:33 [INFO] {'Code': 'AlarmLocal', 'action': 'Stop'}

08/05/2021 19:06:33 [INFO] {'Code': 'CallNoAnswered', 'action': 'Stop'}

08/05/2021 19:06:34 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:35 [INFO] {'Code': 'NewFile', 'action': 'Pulse', 'index': '0', 'data': {'File': '\\/mnt\\/sd\\/2021-05-05\\/001\\/dav\\/19\\/19.21.00-19.21.00[R][0@0][0].mp4_', 'Size': '117159593,', 'StoragePoint': 'NULL'}}

08/05/2021 19:06:36 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:36 [INFO] Fetching storage sensors...

08/05/2021 19:06:39 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:39 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:46 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:46 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:46 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:47 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:48 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:48 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:49 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:06:52 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:07:21 [INFO] {'Code': 'VideoMotion', 'action': 'Stop', 'index': '0', 'data': {'Id': '[', 'RegionName': '['}}

08/05/2021 19:07:36 [INFO] Fetching storage sensors...

08/05/2021 19:07:37 [INFO] {'Code': 'PhoneCallDetect', 'action': 'Stop'}

08/05/2021 19:07:37 [INFO] {'Code': '_CallRemoveMask', 'action': 'Stop'}

08/05/2021 19:08:36 [INFO] Fetching storage sensors...

08/05/2021 19:09:28 [INFO] {'Code': 'TimeChange', 'action': 'Pulse', 'index': '0', 'data': {'BeforeModifyTime': '2021-05-08 14:09:28', 'ModifiedTime': '2021-05-08 14:09:28'}}

08/05/2021 19:09:28 [INFO] {'Code': 'NTPAdjustTime', 'action': 'Pulse', 'index': '0', 'data': {'Address': '200.160.0.8', 'Before': '2021-05-08 14:09:27', 'result': 'true'}}

08/05/2021 19:09:36 [INFO] Fetching storage sensors...

08/05/2021 19:10:36 [INFO] Fetching storage sensors...

08/05/2021 19:11:00 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:11:00 [INFO] {'Code': 'VideoMotion', 'action': 'Start', 'index': '0', 'data': {'Id': '[', 'RegionName': '['}}

08/05/2021 19:11:00 [INFO] {'Code': 'StorageChange', 'action': 'Pulse', 'index': '0', 'data': {'Group': 'ReadWrite', 'Path': '\\/mnt\\/sd'}}

08/05/2021 19:11:00 [INFO] {'Code': 'StorageChange', 'action': 'Pulse', 'index': '0', 'data': {'Group': 'ReadWrite', 'Path': '\\/mnt\\/sd'}}

08/05/2021 19:11:00 [INFO] {'Code': 'StorageChange', 'action': 'Pulse', 'index': '0', 'data': {'Group': 'ReadWrite', 'Path': '\\/mnt\\/sd'}}

08/05/2021 19:11:03 [INFO] {'Code': 'StorageChange', 'action': 'Pulse', 'index': '0', 'data': {'Group': 'ReadWrite', 'Path': '\\/mnt\\/sd'}}

08/05/2021 19:11:04 [INFO] {'Code': 'NewFile', 'action': 'Pulse', 'index': '0', 'data': {'File': '\\/mnt\\/sd\\/2021-05-08\\/001\\/jpg\\/14\\/11\\/04[M][0@0][0].jpg', 'Size': '48281,', 'StoragePoint': 'NULL'}}

08/05/2021 19:11:05 [INFO] {'Code': 'VideoMotionInfo', 'action': 'State'}

08/05/2021 19:11:17 [INFO] {'Code': 'NewFile', 'action': 'Pulse', 'index': '0', 'data': {'File': '\\/mnt\\/sd\\/2021-05-05\\/001\\/dav\\/19\\/19.21.00-19.21.00[R][0@0][0].mp4_', 'Size': '117159593,', 'StoragePoint': 'NULL'}}

08/05/2021 19:11:35 [INFO] {'Code': 'VideoMotion', 'action': 'Stop', 'index': '0', 'data': {'Id': '[', 'RegionName': '['}}

08/05/2021 19:11:36 [INFO] Fetching storage sensors...

08/05/2021 19:12:36 [INFO] Fetching storage sensors...
iankaufmann commented 3 years ago

I made this quick and dirty fix to amcrest2mqtt.py and passed it into the container and it fixed the issue for me:

try:
    for code, payload in camera.event_actions("All", retries=5):
        if code == "ProfileAlarmTransmit":
            motion_payload = "on" if payload["action"] == "Start" else "off"
            mqtt_publish(topics["motion"], motion_payload)
        elif code == "VideoMotion":
            motion_payload = "on" if payload["action"] == "Start" else "off"
            mqtt_publish(topics["motion"], motion_payload)
        elif code == "_DoTalkAction_":
            doorbell_payload = "on" if payload["data"]["Action"] == "Invite" else "off"
            mqtt_publish(topics["doorbell"], doorbell_payload)

        mqtt_publish(topics["event"], payload, json=True)
        log(str(payload))

except AmcrestError as error:
    log(f"Amcrest error {error}", level="ERROR")
    exit_gracefully(1)
dchesterton commented 3 years ago

Thanks for the report. I've pushed an update which should fix this for you.

iankaufmann commented 3 years ago

Hey again. Thanks for the update!

I started messing with the Human Detection on my AD410, so I wanted to pass those as different events / sensors into HA.

I made an update to amcrest2mqtt.py to accommodate this and thought I'd pass it along.

Human Detection Payload (Start)

{'Code': 'CrossRegionDetection', 'action': 'Start', 'index': '0', 'data': {'Action': 'Appear', 'CfgRuleId': '1,', 'Class': 'Normal', 'CountInGroup': '0,', 'DetectRegion': '[', 'EventID': '10173,', 'EventSeq': '2,', 'FrameSequence': '7588437,', 'GroupID': '2,', 'Mark': '0,', 'Name': 'IVS-1', 'Object': '{', 'Age': '0,', 'Angle': '0,', 'Bag': '0,', 'BagType': '0,', 'BoundingBox': '[', 'CarrierBag': '0,', 'Center': '[', 'Confidence': '0,', 'DownClothes': '0,', 'Express': '0,', 'FaceFlag': '0,', 'FaceRect': '[', 'Gender': '0,', 'Glass': '0,', 'HairStyle': '0,', 'HasHat': '0,', 'Helmet': '0,', 'HumanRect': '[', 'LowerBodyColor': '[', 'MainColor': '[', 'MessengerBag': '0,', 'ObjectID': '1450,', 'ObjectType': 'Human', 'Phone': '0,', 'RelativeID': '0,', 'SerialUUID': '', 'ShoulderBag': '0,', 'Source': '-1.0,', 'Speed': '0,', 'SpeedTypeInternal': '0,', 'Umbrella': '0,', 'UpClothes': '0,', 'UpperBodyColor': '[', 'UpperPattern': '0', 'PTS': '43977456400.0,', 'Priority': '0,', 'RuleID': '1,', 'RuleId': '1,', 'Track': '[],', 'UTC': '1621889450,', 'UTCMS': '317'}}

Human Detection Payload (Start)

{'Code': 'CrossRegionDetection', 'action': 'Stop', 'index': '0', 'data': {'Action': 'Appear', 'CfgRuleId': '1,', 'Class': 'Normal', 'CountInGroup': '0,', 'DetectRegion': '[', 'EventID': '10173,', 'EventSeq': '2,', 'FrameSequence': '7588437,', 'GroupID': '2,', 'Mark': '0,', 'Name': 'IVS-1', 'Object': '{', 'Age': '0,', 'Angle': '0,', 'Bag': '0,', 'BagType': '0,', 'BoundingBox': '[', 'CarrierBag': '0,', 'Center': '[', 'Confidence': '0,', 'DownClothes': '0,', 'Express': '0,', 'FaceFlag': '0,', 'FaceRect': '[', 'Gender': '0,', 'Glass': '0,', 'HairStyle': '0,', 'HasHat': '0,', 'Helmet': '0,', 'HumanRect': '[', 'LowerBodyColor': '[', 'MainColor': '[', 'MessengerBag': '0,', 'ObjectID': '1450,', 'ObjectType': 'Human', 'Phone': '0,', 'RelativeID': '0,', 'SerialUUID': '', 'ShoulderBag': '0,', 'Source': '-1.0,', 'Speed': '0,', 'SpeedTypeInternal': '0,', 'Umbrella': '0,', 'UpClothes': '0,', 'UpperBodyColor': '[', 'UpperPattern': '0', 'PTS': '43977456400.0,', 'Priority': '0,', 'RuleID': '1,', 'RuleId': '1,', 'Track': '[],', 'UTC': '1621889450,', 'UTCMS': '317'}}

Updated Code

from slugify import slugify
from amcrest import AmcrestCamera, AmcrestError
from datetime import datetime, timezone
import paho.mqtt.client as mqtt
import os
import sys
from json import dumps
import signal
from threading import Timer

storage_sensors_interval = 60  # 1 hour
is_exiting = False
mqtt_client = None

# Read env variables
amcrest_host = os.getenv("AMCREST_HOST")
amcrest_port = int(os.getenv("AMCREST_PORT") or 80)
amcrest_username = os.getenv("AMCREST_USERNAME") or "admin"
amcrest_password = os.getenv("AMCREST_PASSWORD")

mqtt_host = os.getenv("MQTT_HOST") or "localhost"
mqtt_qos = int(os.getenv("MQTT_QOS") or 0)
mqtt_port = int(os.getenv("MQTT_PORT") or 1883)
mqtt_username = os.getenv("MQTT_USERNAME")
mqtt_password = os.getenv("MQTT_PASSWORD")  # can be None

home_assistant = os.getenv("HOME_ASSISTANT") == "true"
home_assistant_prefix = os.getenv("HOME_ASSISTANT_PREFIX") or "homeassistant"

# Exit if any of the required vars are not provided
if amcrest_host is None:
    log("Please set the AMCREST_HOST environment variable", level="ERROR")
    sys.exit(1)

if amcrest_password is None:
    log("Please set the AMCREST_PASSWORD environment variable", level="ERROR")
    sys.exit(1)

if mqtt_username is None:
    log("Please set the MQTT_USERNAME environment variable", level="ERROR")
    sys.exit(1)

# Helper functions and callbacks
def log(msg, level="INFO"):
    ts = datetime.now(timezone.utc).strftime("%d/%m/%Y %H:%M:%S")
    print(f"{ts} [{level}] {msg}")

def mqtt_publish(topic, payload, exit_on_error=True, json=False):
    global mqtt_client

    msg = mqtt_client.publish(
        topic, payload=(dumps(payload) if json else payload), qos=mqtt_qos, retain=True
    )

    if msg.rc == mqtt.MQTT_ERR_SUCCESS:
        msg.wait_for_publish()
        return msg

    log(f"Error publishing MQTT message: {mqtt.error_string(msg.rc)}", level="ERROR")

    if exit_on_error:
        exit_gracefully(msg.rc, skip_mqtt=True)

def on_mqtt_disconnect(client, userdata, rc):
    if rc != 0:
        log(f"Unexpected MQTT disconnection", level="ERROR")
        exit_gracefully(rc, skip_mqtt=True)

def exit_gracefully(rc, skip_mqtt=False):
    global topics, mqtt_client

    if mqtt_client is not None and mqtt_client.is_connected() and skip_mqtt == False:
        mqtt_publish(topics["status"], "offline", exit_on_error=False)
        mqtt_client.loop_stop(force=True)
        mqtt_client.disconnect()

    # Use os._exit instead of sys.exit to ensure an MQTT disconnect event causes the program to exit correctly as they
    # occur on a separate thread
    os._exit(rc)

def refresh_storage_sensors():
    global camera, topics, storage_sensors_interval

    Timer(storage_sensors_interval, refresh_storage_sensors).start()
    log("Fetching storage sensors...")

    try:
        storage = camera.storage_all
        mqtt_publish(topics["storage_used_percent"], str(storage["used_percent"]))
        mqtt_publish(topics["storage_used"], str(storage["used"][0]))
        mqtt_publish(topics["storage_total"], str(storage["total"][0]))
    except AmcrestError as error:
        log(f"Error fetching storage information {error}", level="WARNING")

def signal_handler(sig, frame):
    # exit immediately upon receiving a second SIGINT
    global is_exiting

    if is_exiting:
        os._exit(1)

    is_exiting = True
    exit_gracefully(0)

signal.signal(signal.SIGINT, signal_handler)

# Connect to camera
camera = AmcrestCamera(
    amcrest_host, amcrest_port, amcrest_username, amcrest_password
).camera

log("Fetching camera details...")

device_type = camera.device_type.replace("type=", "").strip()
is_doorbell = device_type in ["AD110", "AD410"]
serial_number = camera.serial_number.strip()
sw_version = camera.software_information[0].replace("version=", "").strip()
device_name = camera.machine_name.replace("name=", "").strip()
device_slug = slugify(device_name, separator="_")

log(f"Device type: {device_type}")
log(f"Serial number: {serial_number}")
log(f"Software version: {sw_version}")
log(f"Device name: {device_name}")

# MQTT topics
topics = {
    "status": f"amcrest2mqtt/{serial_number}/status",
    "event": f"amcrest2mqtt/{serial_number}/event",
    "motion": f"amcrest2mqtt/{serial_number}/motion",
    "human": f"amcrest2mqtt/{serial_number}/human",
    "doorbell": f"amcrest2mqtt/{serial_number}/doorbell",
    "storage_used": f"amcrest2mqtt/{serial_number}/storage/used",
    "storage_used_percent": f"amcrest2mqtt/{serial_number}/storage/used_percent",
    "storage_total": f"amcrest2mqtt/{serial_number}/storage/total",
    "home_assistant": {
        "doorbell": f"{home_assistant_prefix}/binary_sensor/amcrest2mqtt-{serial_number}/{device_slug}_doorbell/config",
        "motion": f"{home_assistant_prefix}/binary_sensor/amcrest2mqtt-{serial_number}/{device_slug}_motion/config",
        "human": f"{home_assistant_prefix}/binary_sensor/amcrest2mqtt-{serial_number}/{device_slug}_human/config",
        "storage_used": f"{home_assistant_prefix}/sensor/amcrest2mqtt-{serial_number}/{device_slug}_storage_used/config",
        "storage_used_percent": f"{home_assistant_prefix}/sensor/amcrest2mqtt-{serial_number}/{device_slug}_storage_used_percent/config",
        "storage_total": f"{home_assistant_prefix}/sensor/amcrest2mqtt-{serial_number}/{device_slug}_storage_total/config",
    },
}

# Connect to MQTT
mqtt_client = mqtt.Client(
    client_id=f"amcrest2mqtt_{serial_number}", clean_session=False
)
mqtt_client.on_disconnect = on_mqtt_disconnect
mqtt_client.username_pw_set(mqtt_username, password=mqtt_password)
mqtt_client.will_set(topics["status"], payload="offline", qos=mqtt_qos, retain=True)

try:
    mqtt_client.connect(mqtt_host, port=mqtt_port)
    mqtt_client.loop_start()
except ConnectionError as error:
    log(f"Could not connect to MQTT server: {error}", level="ERROR")
    sys.exit(1)

# Configure Home Assistant
if home_assistant:
    log("Writing Home Assistant discovery config...")

    base_config = {
        "availability_topic": topics["status"],
        "qos": mqtt_qos,
        "device": {
            "name": f"Amcrest {device_type}",
            "manufacturer": "Amcrest",
            "model": device_type,
            "identifiers": serial_number,
            "sw_version": sw_version,
            "via_device": "amcrest2mqtt",
        },
    }

    if is_doorbell:
        mqtt_publish(
            topics["home_assistant"]["doorbell"],
            base_config
            | {
                "state_topic": topics["doorbell"],
                "payload_on": "on",
                "payload_off": "off",
                "name": f"{device_name} Doorbell",
                "unique_id": f"{serial_number}.doorbell",
            },
            json=True,
        )

    mqtt_publish(
        topics["home_assistant"]["motion"],
        base_config
        | {
            "state_topic": topics["motion"],
            "payload_on": "on",
            "payload_off": "off",
            "device_class": "motion",
            "name": f"{device_name} Motion",
            "unique_id": f"{serial_number}.motion",
        },
        json=True,
    )

    mqtt_publish(
        topics["home_assistant"]["human"],
        base_config
        | {
            "state_topic": topics["human"],
            "payload_on": "on",
            "payload_off": "off",
            "device_class": "human",
            "name": f"{device_name} Human",
            "unique_id": f"{serial_number}.human",
        },
        json=True,
    )

    mqtt_publish(
        topics["home_assistant"]["storage_used_percent"],
        base_config
        | {
            "state_topic": topics["storage_used_percent"],
            "unit_of_measurement": "%",
            "icon": "mdi:micro-sd",
            "name": f"{device_name} Storage Used %",
            "unique_id": f"{serial_number}.storage_used_percent",
        },
        json=True,
    )

    mqtt_publish(
        topics["home_assistant"]["storage_used"],
        base_config
        | {
            "state_topic": topics["storage_used"],
            "unit_of_measurement": "GB",
            "icon": "mdi:micro-sd",
            "name": f"{device_name} Storage Used",
            "unique_id": f"{serial_number}.storage_used",
        },
        json=True,
    )

    mqtt_publish(
        topics["home_assistant"]["storage_total"],
        base_config
        | {
            "state_topic": topics["storage_total"],
            "unit_of_measurement": "GB",
            "icon": "mdi:micro-sd",
            "name": f"{device_name} Storage Total",
            "unique_id": f"{serial_number}.storage_total",
        },
        json=True,
    )

# Main loop
mqtt_publish(topics["status"], "online")
refresh_storage_sensors()

log("Listening for events...")

try:
    for code, payload in camera.event_actions("All", retries=5):
        if (device_type == "AD110" and code == "ProfileAlarmTransmit") or (code == "VideoMotion" and device_type != "AD110"):
            motion_payload = "on" if payload["action"] == "Start" else "off"
            mqtt_publish(topics["motion"], motion_payload)
        elif code == "CrossRegionDetection" and payload["data"]["ObjectType"] == "Human":
            human_payload = "on" if payload["action"] == "Start" else "off"
            mqtt_publish(topics["human"], human_payload)     
        elif code == "_DoTalkAction_":
            doorbell_payload = "on" if payload["data"]["Action"] == "Invite" else "off"
            mqtt_publish(topics["doorbell"], doorbell_payload)

        mqtt_publish(topics["event"], payload, json=True)
        log(str(payload))

except AmcrestError as error:
    log(f"Amcrest error {error}", level="ERROR")
    exit_gracefully(1)
dchesterton commented 3 years ago

Thanks for the details, I don't have an AD410 yet so that's really useful!

I've used your code to add human detection support to the library. Please can you give it a try when you get a moment?

iankaufmann commented 3 years ago

No prob!

I'm back to running the "official" image and so far so good!

Let me know if there is anything else you want me to test with the AD410... although I can't think of much more we'd need at this point. At least, nothing that is "officially" offered. After poking around, it seems like there is far more going on behind the scenes than is revealed through the Amcrest Smart Home app... but I don't know how useful a lot of it is.

For example, the object detection payload contains all sorts of attributes like "Age", "Bag", "CarrierBag", "Gender", "Hair Style", etc... although they're all zero in what I've observed so far.

There is also a whole HTTP api (which I had to use in order to disable the timestamp overlay on the RTSP feed so that I could use the Blue Iris overlay instead).

https://amcrest.com/forum/viewtopic.php?t=13785&p=32822

It works for the AD110 too so I'm sure you're aware.

Running /cgi-bin/configManager.cgi?action=getConfig&name=All reveals this (all of which looks to be configurable via the HTTP calls):

https://pastebin.com/6zaRJvEZ