|
|
|
@ -13,6 +13,11 @@ import asyncio
|
|
|
|
is_exiting = False
|
|
|
|
is_exiting = False
|
|
|
|
mqtt_client = None
|
|
|
|
mqtt_client = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
config = {}
|
|
|
|
|
|
|
|
cameras = {}
|
|
|
|
|
|
|
|
camera_configs = {}
|
|
|
|
|
|
|
|
camera_topics = {}
|
|
|
|
|
|
|
|
|
|
|
|
# Read env variables
|
|
|
|
# Read env variables
|
|
|
|
amcrest_hosts = os.getenv("AMCREST_HOSTS")
|
|
|
|
amcrest_hosts = os.getenv("AMCREST_HOSTS")
|
|
|
|
amcrest_port = int(os.getenv("AMCREST_PORT") or 80)
|
|
|
|
amcrest_port = int(os.getenv("AMCREST_PORT") or 80)
|
|
|
|
@ -70,15 +75,20 @@ def mqtt_publish(topic, payload, exit_on_error=True, json=False):
|
|
|
|
|
|
|
|
|
|
|
|
def on_mqtt_disconnect(client, userdata, rc):
|
|
|
|
def on_mqtt_disconnect(client, userdata, rc):
|
|
|
|
if rc != 0:
|
|
|
|
if rc != 0:
|
|
|
|
log(f"Unexpected MQTT disconnection", level="ERROR")
|
|
|
|
if rc == 5:
|
|
|
|
|
|
|
|
log(f"MQTT connection not authorized", level="ERROR")
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
log(f"Unexpected MQTT disconnection: {rc}", level="ERROR")
|
|
|
|
exit_gracefully(rc, skip_mqtt=True)
|
|
|
|
exit_gracefully(rc, skip_mqtt=True)
|
|
|
|
|
|
|
|
|
|
|
|
def exit_gracefully(rc, skip_mqtt=False):
|
|
|
|
def exit_gracefully(rc, skip_mqtt=False):
|
|
|
|
global topics, mqtt_client
|
|
|
|
global hosts, camera_topics, mqtt_client
|
|
|
|
|
|
|
|
|
|
|
|
log("Exiting app...")
|
|
|
|
log("Exiting app...")
|
|
|
|
|
|
|
|
|
|
|
|
if mqtt_client is not None and mqtt_client.is_connected() and skip_mqtt == False:
|
|
|
|
if mqtt_client is not None and mqtt_client.is_connected() and skip_mqtt == False:
|
|
|
|
|
|
|
|
for host in hosts:
|
|
|
|
|
|
|
|
topics = camera_topics[host]
|
|
|
|
mqtt_publish(topics["status"], "offline", exit_on_error=False)
|
|
|
|
mqtt_publish(topics["status"], "offline", exit_on_error=False)
|
|
|
|
mqtt_client.disconnect()
|
|
|
|
mqtt_client.disconnect()
|
|
|
|
|
|
|
|
|
|
|
|
@ -86,14 +96,16 @@ def exit_gracefully(rc, skip_mqtt=False):
|
|
|
|
# occur on a separate thread
|
|
|
|
# occur on a separate thread
|
|
|
|
os._exit(rc)
|
|
|
|
os._exit(rc)
|
|
|
|
|
|
|
|
|
|
|
|
def refresh_storage_sensors():
|
|
|
|
def refresh_storage_sensors(hosts, cameras, camera_topics):
|
|
|
|
global camera, topics, storage_poll_interval
|
|
|
|
global storage_poll_interval
|
|
|
|
|
|
|
|
|
|
|
|
Timer(storage_poll_interval, refresh_storage_sensors).start()
|
|
|
|
Timer(storage_poll_interval, refresh_storage_sensors).start()
|
|
|
|
log("Fetching storage sensors...")
|
|
|
|
log("Fetching storage sensors...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for host in hosts:
|
|
|
|
|
|
|
|
topics = camera_topics[host]
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
storage = camera.storage_all
|
|
|
|
storage = cameras[host].storage_all
|
|
|
|
|
|
|
|
|
|
|
|
mqtt_publish(topics["storage_used_percent"], str(storage["used_percent"]))
|
|
|
|
mqtt_publish(topics["storage_used_percent"], str(storage["used_percent"]))
|
|
|
|
mqtt_publish(topics["storage_used"], to_gb(storage["used"]))
|
|
|
|
mqtt_publish(topics["storage_used"], to_gb(storage["used"]))
|
|
|
|
@ -115,19 +127,21 @@ def signal_handler(sig, frame):
|
|
|
|
exit_gracefully(0)
|
|
|
|
exit_gracefully(0)
|
|
|
|
|
|
|
|
|
|
|
|
def get_camera(amcrest_host, amcrest_post, amcrest_username, amcrest_password, device_name):
|
|
|
|
def get_camera(amcrest_host, amcrest_post, amcrest_username, amcrest_password, device_name):
|
|
|
|
camera |= AmcrestCamera(
|
|
|
|
camera = AmcrestCamera(
|
|
|
|
amcrest_host, amcrest_port, amcrest_username, amcrest_password
|
|
|
|
amcrest_host, amcrest_port, amcrest_username, amcrest_password
|
|
|
|
).camera
|
|
|
|
).camera
|
|
|
|
|
|
|
|
|
|
|
|
# Fetch camera details
|
|
|
|
# Fetch camera details
|
|
|
|
log("Fetching camera details...")
|
|
|
|
log("Fetching camera details...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
camera_config = {}
|
|
|
|
|
|
|
|
camera_config["device_name"] = device_name
|
|
|
|
camera_config["amcrest_host"] = amcrest_host
|
|
|
|
camera_config["amcrest_host"] = amcrest_host
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
camera_config["device_type"] = device_type = camera.device_type.replace("type=", "").strip()
|
|
|
|
camera_config["device_type"] = device_type = camera.device_type.replace("type=", "").strip()
|
|
|
|
is_ad110 = camera_config["device_type"] == "AD110"
|
|
|
|
is_ad110 = camera_config["device_type"] == "AD110"
|
|
|
|
is_ad410 = camera_config["device_type"] == "AD410"
|
|
|
|
camera_config["is_ad410"] = is_ad410 = camera_config["device_type"] == "AD410"
|
|
|
|
is_doorbell = is_ad110 or is_ad410
|
|
|
|
camera_config["is_doorbell"] = is_doorbell = is_ad110 or is_ad410
|
|
|
|
camera_config["serial_number"] = serial_number = camera.serial_number
|
|
|
|
camera_config["serial_number"] = serial_number = camera.serial_number
|
|
|
|
|
|
|
|
|
|
|
|
if not isinstance(serial_number, str):
|
|
|
|
if not isinstance(serial_number, str):
|
|
|
|
@ -142,7 +156,7 @@ def get_camera(amcrest_host, amcrest_post, amcrest_username, amcrest_password, d
|
|
|
|
if not device_name:
|
|
|
|
if not device_name:
|
|
|
|
device_name = camera.machine_name.replace("name=", "").strip()
|
|
|
|
device_name = camera.machine_name.replace("name=", "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
device_slug = slugify(device_name, separator="_")
|
|
|
|
camera_config["device_slug"] = device_slug = slugify(device_name, separator="_")
|
|
|
|
except AmcrestError as error:
|
|
|
|
except AmcrestError as error:
|
|
|
|
log(f"Error fetching camera details", level="ERROR")
|
|
|
|
log(f"Error fetching camera details", level="ERROR")
|
|
|
|
exit_gracefully(1)
|
|
|
|
exit_gracefully(1)
|
|
|
|
@ -152,8 +166,10 @@ def get_camera(amcrest_host, amcrest_post, amcrest_username, amcrest_password, d
|
|
|
|
log(f"Software version: {amcrest_version}")
|
|
|
|
log(f"Software version: {amcrest_version}")
|
|
|
|
log(f"Device name: {device_name}")
|
|
|
|
log(f"Device name: {device_name}")
|
|
|
|
|
|
|
|
|
|
|
|
# MQTT topics
|
|
|
|
setup = {}
|
|
|
|
topics = {
|
|
|
|
setup["camera"] = camera
|
|
|
|
|
|
|
|
setup["camera_config"] = camera_config
|
|
|
|
|
|
|
|
setup["camera_topic"] = {
|
|
|
|
"config": f"amcrest2mqtt/{serial_number}/config",
|
|
|
|
"config": f"amcrest2mqtt/{serial_number}/config",
|
|
|
|
"status": f"amcrest2mqtt/{serial_number}/status",
|
|
|
|
"status": f"amcrest2mqtt/{serial_number}/status",
|
|
|
|
"event": f"amcrest2mqtt/{serial_number}/event",
|
|
|
|
"event": f"amcrest2mqtt/{serial_number}/event",
|
|
|
|
@ -187,9 +203,12 @@ def get_camera(amcrest_host, amcrest_post, amcrest_username, amcrest_password, d
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return camera, camera_config, topics
|
|
|
|
return setup
|
|
|
|
|
|
|
|
|
|
|
|
def config_home_assistant(config, camera_config, topics):
|
|
|
|
def config_home_assistant(config, camera_config, topics):
|
|
|
|
|
|
|
|
amcrest_version = config["amcrest_version"]
|
|
|
|
|
|
|
|
device_name = camera_config["device_name"]
|
|
|
|
|
|
|
|
device_slug = camera_config["device_slug"]
|
|
|
|
device_type = camera_config["device_type"]
|
|
|
|
device_type = camera_config["device_type"]
|
|
|
|
serial_number = camera_config["serial_number"]
|
|
|
|
serial_number = camera_config["serial_number"]
|
|
|
|
|
|
|
|
|
|
|
|
@ -206,7 +225,7 @@ def config_home_assistant(config, camera_config, topics):
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if is_doorbell:
|
|
|
|
if camera_config["is_doorbell"]:
|
|
|
|
doorbell_name = "Doorbell" if device_name == "Doorbell" else f"{device_name} Doorbell"
|
|
|
|
doorbell_name = "Doorbell" if device_name == "Doorbell" else f"{device_name} Doorbell"
|
|
|
|
|
|
|
|
|
|
|
|
mqtt_publish(topics["home_assistant_legacy"]["doorbell"], "")
|
|
|
|
mqtt_publish(topics["home_assistant_legacy"]["doorbell"], "")
|
|
|
|
@ -224,7 +243,7 @@ def config_home_assistant(config, camera_config, topics):
|
|
|
|
json=True,
|
|
|
|
json=True,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if is_ad410:
|
|
|
|
if camera_config["is_ad410"]:
|
|
|
|
mqtt_publish(topics["home_assistant_legacy"]["human"], "")
|
|
|
|
mqtt_publish(topics["home_assistant_legacy"]["human"], "")
|
|
|
|
mqtt_publish(
|
|
|
|
mqtt_publish(
|
|
|
|
topics["home_assistant"]["human"],
|
|
|
|
topics["home_assistant"]["human"],
|
|
|
|
@ -351,14 +370,21 @@ def config_home_assistant(config, camera_config, topics):
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def camera_online(config, camera_config, topics):
|
|
|
|
def camera_online(config, camera_config, topics):
|
|
|
|
|
|
|
|
amcrest_version = config["amcrest_version"]
|
|
|
|
|
|
|
|
host = camera_config["amcrest_host"]
|
|
|
|
|
|
|
|
device_name = camera_config["device_name"]
|
|
|
|
|
|
|
|
device_slug = camera_config["device_slug"]
|
|
|
|
|
|
|
|
device_type = camera_config["device_type"]
|
|
|
|
|
|
|
|
serial_number = camera_config["serial_number"]
|
|
|
|
|
|
|
|
|
|
|
|
mqtt_publish(topics["status"], "online")
|
|
|
|
mqtt_publish(topics["status"], "online")
|
|
|
|
mqtt_publish(topics["config"], {
|
|
|
|
mqtt_publish(topics["config"], {
|
|
|
|
"version": version,
|
|
|
|
"version": version,
|
|
|
|
"device_type": camera_config["device_type"],
|
|
|
|
"device_type": device_type,
|
|
|
|
"device_name": device_name,
|
|
|
|
"device_name": device_name,
|
|
|
|
"sw_version": config["amcrest_version"],
|
|
|
|
"sw_version": amcrest_version,
|
|
|
|
"serial_number": camera_config["serial_number"],
|
|
|
|
"serial_number": serial_number,
|
|
|
|
"host": camera_config["amcrest_host"],
|
|
|
|
"host": host,
|
|
|
|
}, json=True)
|
|
|
|
}, json=True)
|
|
|
|
|
|
|
|
|
|
|
|
# Exit if any of the required vars are not provided
|
|
|
|
# Exit if any of the required vars are not provided
|
|
|
|
@ -394,11 +420,14 @@ log(f"App Version: {version}")
|
|
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
|
|
|
|
|
|
|
|
|
|
# Connect to each camera, if not already
|
|
|
|
# Connect to each camera, if not already
|
|
|
|
for x in range(1..host_count):
|
|
|
|
for host in hosts:
|
|
|
|
if cameras[x] and camers[x].serial_number:
|
|
|
|
log(f"Working host: {host}", level="INFO")
|
|
|
|
|
|
|
|
if host in cameras and camers[host].serial_number:
|
|
|
|
continue
|
|
|
|
continue
|
|
|
|
cameras[x], camera_configs[x], camera_topics[x] = get_camera(hosts[x], amcrest_port, amcrest_username, amcrest_password, names[x])
|
|
|
|
setup = get_camera(host, amcrest_port, amcrest_username, amcrest_password, names.pop())
|
|
|
|
camera_count = len(cameras)
|
|
|
|
cameras[host] = setup["camera"]
|
|
|
|
|
|
|
|
camera_configs[host] = setup["camera_config"]
|
|
|
|
|
|
|
|
camera_topics[host] = setup["camera_topic"]
|
|
|
|
|
|
|
|
|
|
|
|
# Connect to MQTT
|
|
|
|
# Connect to MQTT
|
|
|
|
mqtt_client = mqtt.Client(
|
|
|
|
mqtt_client = mqtt.Client(
|
|
|
|
@ -406,9 +435,9 @@ mqtt_client = mqtt.Client(
|
|
|
|
)
|
|
|
|
)
|
|
|
|
mqtt_client.on_disconnect = on_mqtt_disconnect
|
|
|
|
mqtt_client.on_disconnect = on_mqtt_disconnect
|
|
|
|
# send "will_set" for each connected camera
|
|
|
|
# send "will_set" for each connected camera
|
|
|
|
for x in range(1..camera_count):
|
|
|
|
for host in hosts:
|
|
|
|
if camera_topics[x]["status"]:
|
|
|
|
if camera_topics[host]["status"]:
|
|
|
|
mqtt_client.will_set(camera_topics[x]["status"], payload="offline", qos=config["mqtt_qos"], retain=True)
|
|
|
|
mqtt_client.will_set(camera_topics[host]["status"], payload="offline", qos=config["mqtt_qos"], retain=True)
|
|
|
|
|
|
|
|
|
|
|
|
if mqtt_tls_enabled:
|
|
|
|
if mqtt_tls_enabled:
|
|
|
|
log(f"Setting up MQTT for TLS")
|
|
|
|
log(f"Setting up MQTT for TLS")
|
|
|
|
@ -442,35 +471,35 @@ except ConnectionError as error:
|
|
|
|
if home_assistant:
|
|
|
|
if home_assistant:
|
|
|
|
log("Writing Home Assistant discovery config...")
|
|
|
|
log("Writing Home Assistant discovery config...")
|
|
|
|
|
|
|
|
|
|
|
|
for x in range(1..camera_count):
|
|
|
|
for host in hosts:
|
|
|
|
if camera_topics[x]["status"]:
|
|
|
|
if host in camera_topics:
|
|
|
|
config_home_assistant(camera_configs[x], camera_topics[x])
|
|
|
|
config_home_assistant(config, camera_configs[host], camera_topics[host])
|
|
|
|
|
|
|
|
|
|
|
|
# Main loop
|
|
|
|
# Main loop
|
|
|
|
for x in range(1..camera_count):
|
|
|
|
for host in hosts:
|
|
|
|
if camera_topics[x]["status"]:
|
|
|
|
if host in camera_topics:
|
|
|
|
camera_online(config, camera_configs[x], camera_topics[x])
|
|
|
|
camera_online(config, camera_configs[host], camera_topics[host])
|
|
|
|
|
|
|
|
|
|
|
|
if storage_poll_interval > 0:
|
|
|
|
if storage_poll_interval > 0:
|
|
|
|
refresh_storage_sensors()
|
|
|
|
refresh_storage_sensors(hosts, cameras, camera_topics)
|
|
|
|
|
|
|
|
|
|
|
|
log("Listening for events...")
|
|
|
|
log("Listening for events...")
|
|
|
|
|
|
|
|
|
|
|
|
async def main():
|
|
|
|
async def main():
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
for x in range(1..camera_count):
|
|
|
|
for host in hosts:
|
|
|
|
async for code, payload in cameras[x].async_event_actions("All"):
|
|
|
|
async for code, payload in cameras[host].async_event_actions("All"):
|
|
|
|
if (camera_config[x].is_ad110 and code == "ProfileAlarmTransmit") or (code == "VideoMotion" and not camera_config[x].is_ad110):
|
|
|
|
if (camera_configs[host].is_ad110 and code == "ProfileAlarmTransmit") or (code == "VideoMotion" and not camera_configs[host].is_ad110):
|
|
|
|
motion_payload = "on" if payload["action"] == "Start" else "off"
|
|
|
|
motion_payload = "on" if payload["action"] == "Start" else "off"
|
|
|
|
mqtt_publish(camera_topics[x]["motion"], motion_payload)
|
|
|
|
mqtt_publish(camera_topics[host]["motion"], motion_payload)
|
|
|
|
elif code == "CrossRegionDetection" and payload["data"]["ObjectType"] == "Human":
|
|
|
|
elif code == "CrossRegionDetection" and payload["data"]["ObjectType"] == "Human":
|
|
|
|
human_payload = "on" if payload["action"] == "Start" else "off"
|
|
|
|
human_payload = "on" if payload["action"] == "Start" else "off"
|
|
|
|
mqtt_publish(camera_topics[x]["human"], human_payload)
|
|
|
|
mqtt_publish(camera_topics[host]["human"], human_payload)
|
|
|
|
elif code == "_DoTalkAction_":
|
|
|
|
elif code == "_DoTalkAction_":
|
|
|
|
doorbell_payload = "on" if payload["data"]["Action"] == "Invite" else "off"
|
|
|
|
doorbell_payload = "on" if payload["data"]["Action"] == "Invite" else "off"
|
|
|
|
mqtt_publish(camera_topics[x]["doorbell"], doorbell_payload)
|
|
|
|
mqtt_publish(camera_topics[host]["doorbell"], doorbell_payload)
|
|
|
|
|
|
|
|
|
|
|
|
mqtt_publish(camera_topics[x]["event"], payload, json=True)
|
|
|
|
mqtt_publish(camera_topics[host]["event"], payload, json=True)
|
|
|
|
log(str(payload))
|
|
|
|
log(str(payload))
|
|
|
|
|
|
|
|
|
|
|
|
except AmcrestError as error:
|
|
|
|
except AmcrestError as error:
|
|
|
|
|