code cleanup

pull/106/head
Jeff Culverhouse 12 months ago
parent d322f9ad4e
commit d46be67b57

@ -4,6 +4,7 @@ from datetime import datetime, timezone
import paho.mqtt.client as mqtt import paho.mqtt.client as mqtt
import os import os
import sys import sys
import time
from json import dumps from json import dumps
import signal import signal
from threading import Timer from threading import Timer
@ -14,9 +15,7 @@ is_exiting = False
mqtt_client = None mqtt_client = None
config = {} config = {}
cameras = {} devices = {}
camera_configs = {}
camera_topics = {}
# Read env variables # Read env variables
amcrest_hosts = os.getenv("AMCREST_HOSTS") amcrest_hosts = os.getenv("AMCREST_HOSTS")
@ -82,14 +81,13 @@ def on_mqtt_disconnect(client, userdata, rc):
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 hosts, camera_topics, mqtt_client global hosts, devices, 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: for host in hosts:
topics = camera_topics[host] mqtt_publish(devices[host]["topics"]["status"], "offline", exit_on_error=False)
mqtt_publish(topics["status"], "offline", exit_on_error=False)
mqtt_client.disconnect() mqtt_client.disconnect()
# Use os._exit instead of sys.exit to ensure an MQTT disconnect event causes the program to exit correctly as they # Use os._exit instead of sys.exit to ensure an MQTT disconnect event causes the program to exit correctly as they
@ -97,21 +95,21 @@ def exit_gracefully(rc, skip_mqtt=False):
os._exit(rc) os._exit(rc)
def refresh_storage_sensors(): def refresh_storage_sensors():
global hosts, camera, camera_topics, storage_poll_interval global hosts, devices, 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: for host in hosts:
topics = camera_topics[host] topics = devices[host]["topics"]
try: try:
storage = cameras[host].storage_all storage = devices[host]["camera"].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"]))
mqtt_publish(topics["storage_total"], to_gb(storage["total"])) mqtt_publish(topics["storage_total"], to_gb(storage["total"]))
except AmcrestError as error: except AmcrestError as error:
log(f"Error fetching storage information {error}", level="WARNING") log(f"Error fetching storage information for {host}: {error}", level="WARNING")
def to_gb(total): def to_gb(total):
return str(round(float(total[0]) / 1024 / 1024 / 1024, 2)) return str(round(float(total[0]) / 1024 / 1024 / 1024, 2))
@ -132,33 +130,27 @@ def get_camera(amcrest_host, amcrest_post, amcrest_username, amcrest_password, d
).camera ).camera
# Fetch camera details # Fetch camera details
log("Fetching camera details...") log(f"Fetching camera details for {amcrest_host}...")
camera_config = {} device_name = device_name
camera_config["device_name"] = device_name amcrest_host = amcrest_host
camera_config["amcrest_host"] = amcrest_host
try: try:
camera_config["device_type"] = device_type = camera.device_type.replace("type=", "").strip() device_type = camera.device_type.replace("type=", "").strip()
camera_config["is_ad110"] = is_ad110 = camera_config["device_type"] == "AD110" is_ad110 = device_type == "AD110"
camera_config["is_ad410"] = is_ad410 = camera_config["device_type"] == "AD410" is_ad410 = device_type == "AD410"
camera_config["is_doorbell"] = is_doorbell = is_ad110 or is_ad410 is_doorbell = is_ad110 or is_ad410
camera_config["serial_number"] = serial_number = camera.serial_number serial_number = camera.serial_number
if not isinstance(serial_number, str): if not isinstance(serial_number, str):
log(f"Error fetching serial number", level="ERROR") log(f"Error fetching serial number for {amcrest_host}", level="ERROR")
exit_gracefully(1) exit_gracefully(1)
sw_version = camera.software_information[0].replace("version=", "").strip() sw_version = camera.software_information[0].replace("version=", "").strip()
build_version = camera.software_information[1].strip() build_version = camera.software_information[1].strip()
amcrest_version = f"{sw_version} ({build_version})"
config["amcrest_version"] = amcrest_version = f"{sw_version} ({build_version})" device_slug = slugify(device_name, separator="_")
if not device_name:
device_name = camera.machine_name.replace("name=", "").strip()
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 for {amcrest_host}", level="ERROR")
exit_gracefully(1) exit_gracefully(1)
log(f"Device type: {device_type}") log(f"Device type: {device_type}")
@ -166,10 +158,21 @@ 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}")
setup = {} setup = {
setup["camera"] = camera "camera": camera,
setup["camera_config"] = camera_config }
setup["camera_topic"] = { setup["config"] = {
"amcrest_host": amcrest_host,
"device_name": device_name,
"device_type": device_type,
"device_slug": device_slug,
"is_ad110": is_ad110,
"is_ad410": is_ad410,
"is_doorbell": is_doorbell,
"serial_number": serial_number,
"amcrest_version": amcrest_version,
}
setup["topics"] = {
"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",
@ -205,15 +208,15 @@ def get_camera(amcrest_host, amcrest_post, amcrest_username, amcrest_password, d
return setup return setup
def config_home_assistant(config, camera_config, topics): def config_home_assistant(config, device):
amcrest_version = config["amcrest_version"] device_name = device["config"]["device_name"]
device_name = camera_config["device_name"] device_type = device["config"]["device_type"]
device_slug = camera_config["device_slug"] device_slug = device["config"]["device_slug"]
device_type = camera_config["device_type"] serial_number = device["config"]["serial_number"]
serial_number = camera_config["serial_number"] amcrest_version = device["config"]["amcrest_version"]
base_config = { base_config = {
"availability_topic": topics["status"], "availability_topic": device["topics"]["status"],
"qos": config["mqtt_qos"], "qos": config["mqtt_qos"],
"device": { "device": {
"name": f"Amcrest {device_type}", "name": f"Amcrest {device_type}",
@ -225,15 +228,15 @@ def config_home_assistant(config, camera_config, topics):
}, },
} }
if camera_config["is_doorbell"]: if device["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(device["topics"]["home_assistant_legacy"]["doorbell"], "")
mqtt_publish( mqtt_publish(
topics["home_assistant"]["doorbell"], device["topics"]["home_assistant"]["doorbell"],
base_config base_config
| { | {
"state_topic": topics["doorbell"], "state_topic": device["topics"]["doorbell"],
"payload_on": "on", "payload_on": "on",
"payload_off": "off", "payload_off": "off",
"icon": "mdi:doorbell", "icon": "mdi:doorbell",
@ -243,13 +246,13 @@ def config_home_assistant(config, camera_config, topics):
json=True, json=True,
) )
if camera_config["is_ad410"]: if device["config"]["is_ad410"]:
mqtt_publish(topics["home_assistant_legacy"]["human"], "") mqtt_publish(device["topics"]["home_assistant_legacy"]["human"], "")
mqtt_publish( mqtt_publish(
topics["home_assistant"]["human"], device["topics"]["home_assistant"]["human"],
base_config base_config
| { | {
"state_topic": topics["human"], "state_topic": device["topics"]["human"],
"payload_on": "on", "payload_on": "on",
"payload_off": "off", "payload_off": "off",
"device_class": "motion", "device_class": "motion",
@ -259,12 +262,12 @@ def config_home_assistant(config, camera_config, topics):
json=True, json=True,
) )
mqtt_publish(topics["home_assistant_legacy"]["motion"], "") mqtt_publish(device["topics"]["home_assistant_legacy"]["motion"], "")
mqtt_publish( mqtt_publish(
topics["home_assistant"]["motion"], device["topics"]["home_assistant"]["motion"],
base_config base_config
| { | {
"state_topic": topics["motion"], "state_topic": device["topics"]["motion"],
"payload_on": "on", "payload_on": "on",
"payload_off": "off", "payload_off": "off",
"device_class": "motion", "device_class": "motion",
@ -274,12 +277,12 @@ def config_home_assistant(config, camera_config, topics):
json=True, json=True,
) )
mqtt_publish(topics["home_assistant_legacy"]["version"], "") mqtt_publish(device["topics"]["home_assistant_legacy"]["version"], "")
mqtt_publish( mqtt_publish(
topics["home_assistant"]["version"], device["topics"]["home_assistant"]["version"],
base_config base_config
| { | {
"state_topic": topics["config"], "state_topic": device["topics"]["config"],
"value_template": "{{ value_json.sw_version }}", "value_template": "{{ value_json.sw_version }}",
"icon": "mdi:package-up", "icon": "mdi:package-up",
"name": f"{device_name} Version", "name": f"{device_name} Version",
@ -290,12 +293,12 @@ def config_home_assistant(config, camera_config, topics):
json=True, json=True,
) )
mqtt_publish(topics["home_assistant_legacy"]["serial_number"], "") mqtt_publish(device["topics"]["home_assistant_legacy"]["serial_number"], "")
mqtt_publish( mqtt_publish(
topics["home_assistant"]["serial_number"], device["topics"]["home_assistant"]["serial_number"],
base_config base_config
| { | {
"state_topic": topics["config"], "state_topic": device["topics"]["config"],
"value_template": "{{ value_json.serial_number }}", "value_template": "{{ value_json.serial_number }}",
"icon": "mdi:alphabetical-variant", "icon": "mdi:alphabetical-variant",
"name": f"{device_name} Serial Number", "name": f"{device_name} Serial Number",
@ -306,12 +309,12 @@ def config_home_assistant(config, camera_config, topics):
json=True, json=True,
) )
mqtt_publish(topics["home_assistant_legacy"]["host"], "") mqtt_publish(device["topics"]["home_assistant_legacy"]["host"], "")
mqtt_publish( mqtt_publish(
topics["home_assistant"]["host"], device["topics"]["home_assistant"]["host"],
base_config base_config
| { | {
"state_topic": topics["config"], "state_topic": device["topics"]["config"],
"value_template": "{{ value_json.host }}", "value_template": "{{ value_json.host }}",
"icon": "mdi:ip-network", "icon": "mdi:ip-network",
"name": f"{device_name} Host", "name": f"{device_name} Host",
@ -323,12 +326,12 @@ def config_home_assistant(config, camera_config, topics):
) )
if storage_poll_interval > 0: if storage_poll_interval > 0:
mqtt_publish(topics["home_assistant_legacy"]["storage_used_percent"], "") mqtt_publish(device["topics"]["home_assistant_legacy"]["storage_used_percent"], "")
mqtt_publish( mqtt_publish(
topics["home_assistant"]["storage_used_percent"], device["topics"]["home_assistant"]["storage_used_percent"],
base_config base_config
| { | {
"state_topic": topics["storage_used_percent"], "state_topic": device["topics"]["storage_used_percent"],
"unit_of_measurement": "%", "unit_of_measurement": "%",
"icon": "mdi:micro-sd", "icon": "mdi:micro-sd",
"name": f"{device_name} Storage Used %", "name": f"{device_name} Storage Used %",
@ -339,12 +342,12 @@ def config_home_assistant(config, camera_config, topics):
json=True, json=True,
) )
mqtt_publish(topics["home_assistant_legacy"]["storage_used"], "") mqtt_publish(device["topics"]["home_assistant_legacy"]["storage_used"], "")
mqtt_publish( mqtt_publish(
topics["home_assistant"]["storage_used"], device["topics"]["home_assistant"]["storage_used"],
base_config base_config
| { | {
"state_topic": topics["storage_used"], "state_topic": device["topics"]["storage_used"],
"unit_of_measurement": "GB", "unit_of_measurement": "GB",
"icon": "mdi:micro-sd", "icon": "mdi:micro-sd",
"name": f"{device_name} Storage Used", "name": f"{device_name} Storage Used",
@ -354,12 +357,12 @@ def config_home_assistant(config, camera_config, topics):
json=True, json=True,
) )
mqtt_publish(topics["home_assistant_legacy"]["storage_total"], "") mqtt_publish(device["topics"]["home_assistant_legacy"]["storage_total"], "")
mqtt_publish( mqtt_publish(
topics["home_assistant"]["storage_total"], device["topics"]["home_assistant"]["storage_total"],
base_config base_config
| { | {
"state_topic": topics["storage_total"], "state_topic": device["topics"]["storage_total"],
"unit_of_measurement": "GB", "unit_of_measurement": "GB",
"icon": "mdi:micro-sd", "icon": "mdi:micro-sd",
"name": f"{device_name} Storage Total", "name": f"{device_name} Storage Total",
@ -369,22 +372,15 @@ def config_home_assistant(config, camera_config, topics):
json=True, json=True,
) )
def camera_online(config, camera_config, topics): def camera_online(device):
amcrest_version = config["amcrest_version"] mqtt_publish(device["topics"]["status"], "online")
host = camera_config["amcrest_host"] mqtt_publish(device["topics"]["config"], {
device_name = camera_config["device_name"] "version": device["config"]["amcrest_version"],
device_slug = camera_config["device_slug"] "device_type": device["config"]["device_type"],
device_type = camera_config["device_type"] "device_name": device["config"]["device_name"],
serial_number = camera_config["serial_number"] "sw_version": device["config"]["amcrest_version"],
"serial_number": device["config"]["serial_number"],
mqtt_publish(topics["status"], "online") "host": device["config"]["amcrest_host"],
mqtt_publish(topics["config"], {
"version": version,
"device_type": device_type,
"device_name": device_name,
"sw_version": amcrest_version,
"serial_number": serial_number,
"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
@ -401,7 +397,7 @@ names = device_names.split()
name_count = len(names) name_count = len(names)
if host_count != name_count: if host_count != name_count:
log("The AMCREST_HOSTS and DEVICE_NAMES must have the same number of space-delimited devices", level="ERROR") log("The AMCREST_HOSTS and DEVICE_NAMES must have the same number of space-delimited hosts/names", level="ERROR")
sys.exit(1) sys.exit(1)
if amcrest_password is None: if amcrest_password is None:
@ -421,34 +417,31 @@ signal.signal(signal.SIGINT, signal_handler)
# Connect to each camera, if not already # Connect to each camera, if not already
for host in hosts: for host in hosts:
log(f"Working host: {host}", level="INFO") name = names.pop()
if host in cameras and camers[host].serial_number: log(f"Connecting host: {host} as {name}", level="INFO")
continue devices[host] = get_camera(host, amcrest_port, amcrest_username, amcrest_password, name)
setup = get_camera(host, amcrest_port, amcrest_username, amcrest_password, names.pop()) log(f"Connecting to hosts done.", level="INFO")
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(
client_id=f"amcrest2mqtt_broker", clean_session=False client_id=f"amcrest2mqtt_broker", clean_session=False
) )
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 host in hosts: for host in hosts:
if camera_topics[host]["status"]: if devices[host]["topics"]["status"]:
mqtt_client.will_set(camera_topics[host]["status"], payload="offline", qos=config["mqtt_qos"], retain=True) mqtt_client.will_set(devices[host]["topics"]["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")
if mqtt_tls_ca_cert is None: if mqtt_tls_ca_cert is None:
log("Missing var: MQTT_TLS_CA_CERT", level="ERROR") log("Missing env var: MQTT_TLS_CA_CERT", level="ERROR")
sys.exit(1) sys.exit(1)
if mqtt_tls_cert is None: if mqtt_tls_cert is None:
log("Missing var: MQTT_TLS_CERT", level="ERROR") log("Missing env var: MQTT_TLS_CERT", level="ERROR")
sys.exit(1) sys.exit(1)
if mqtt_tls_cert is None: if mqtt_tls_cert is None:
log("Missing var: MQTT_TLS_KEY", level="ERROR") log("Missing env var: MQTT_TLS_KEY", level="ERROR")
sys.exit(1) sys.exit(1)
mqtt_client.tls_set( mqtt_client.tls_set(
ca_certs=mqtt_tls_ca_cert, ca_certs=mqtt_tls_ca_cert,
@ -469,41 +462,44 @@ except ConnectionError as error:
# Configure Home Assistant # Configure Home Assistant
if home_assistant: if home_assistant:
log("Writing Home Assistant discovery config...") log("Writing Home Assistant discovery config...")
for host in hosts: for host in hosts:
if host in camera_topics: config_home_assistant(config, devices[host])
config_home_assistant(config, camera_configs[host], camera_topics[host])
# Main loop # Main loop
for host in hosts: for host in hosts:
if host in camera_topics: camera_online(devices[host])
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()
log("Listening for events...") log(f"Listening for events on {len(hosts)} hosts...", level="INFO")
async def main(): async def main():
try: try:
for host in hosts: for host in hosts:
async for code, payload in cameras[host].async_event_actions("All"): device = devices[host]
if (camera_configs[host]["is_ad110"] and code == "ProfileAlarmTransmit") or (code == "VideoMotion" and not camera_configs[host]["is_ad110"]): config = device["config"]
motion_payload = "on" if payload["action"] == "Start" else "off" camera = device["camera"]
mqtt_publish(camera_topics[host]["motion"], motion_payload) topics = device["topics"]
elif code == "CrossRegionDetection" and payload["data"]["ObjectType"] == "Human": async for code, payload in camera.async_event_actions("All"):
human_payload = "on" if payload["action"] == "Start" else "off" log(f"Event on {host}: {str(payload)}", error="INFO")
mqtt_publish(camera_topics[host]["human"], human_payload) if ((code == "ProfileAlarmTransmit" and config["is_ad110"])
elif code == "_DoTalkAction_": or (code == "VideoMotion" and not config["is_ad110"])):
doorbell_payload = "on" if payload["data"]["Action"] == "Invite" else "off" motion_payload = "on" if payload["action"] == "Start" else "off"
mqtt_publish(camera_topics[host]["doorbell"], doorbell_payload) mqtt_publish(topics["motion"], motion_payload)
elif code == "CrossRegionDetection" and payload["data"]["ObjectType"] == "Human":
mqtt_publish(camera_topics[host]["event"], payload, json=True) human_payload = "on" if payload["action"] == "Start" else "off"
log(str(payload)) mqtt_publish(topics["human"], human_payload)
elif code == "_DoTalkAction_":
except AmcrestError as error: doorbell_payload = "on" if payload["data"]["Action"] == "Invite" else "off"
log(f"Amcrest error: {AmcrestError}", level="ERROR") mqtt_publish(topics["doorbell"], doorbell_payload)
time.sleep(10)
mqtt_publish(topics["event"], payload, json=True)
except AmcrestError as error:
log(f"Amcrest error while working on {host}: {AmcrestError}", level="ERROR")
time.sleep(10)
asyncio.run(main()) asyncio.run(main())

Loading…
Cancel
Save