From 8a1779181c52899bd50256f8fd806fbcd10aba1a Mon Sep 17 00:00:00 2001 From: Undercover Computing Date: Sat, 30 Aug 2025 17:07:23 +1200 Subject: [PATCH] improved efficiency --- docker/docker-compose.yaml | 2 +- docker/main.py | 77 ++++++++++++++++++++++++-------------- main.py | 77 ++++++++++++++++++++++++-------------- 3 files changed, 97 insertions(+), 59 deletions(-) diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index b4a6e1d..5782782 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -11,7 +11,7 @@ services: SMTP_PASSWORD: yourpassword # SMTP password for authentication EMAIL_FROM: user@example.com # Sender email address EMAIL_TO: you@example.com,friend@example.com # Recipient email addresses (comma-separated) - HOMEASSISTANT_URL: https://ha.domain.com # URL to Home Assistant instance (external) + HOMEASSISTANT_URL: https://ha.domain.com # URL to Home Assistant instance (external) (Without a slash at the end) HOMEASSISTANT_IP: http://ha-ip:8123 # Home Assistant internal IP URL MQTT_BROKER_IP: mqtt-ip # MQTT broker IP address MQTT_PORT: 1883 # MQTT broker port diff --git a/docker/main.py b/docker/main.py index b58cee5..2a2fa2d 100644 --- a/docker/main.py +++ b/docker/main.py @@ -16,6 +16,7 @@ logging.basicConfig( ) logger = logging.getLogger(__name__) +# Load config with open('config.json', 'r') as f: config = json.load(f) @@ -33,6 +34,7 @@ MQTT_PORT = config["mqtt"]["port"] MQTT_USERNAME = config["mqtt"]["username"] MQTT_PASSWORD = config["mqtt"]["password"] +# Load alert rules try: with open("alert_rules.json", "r") as f: alert_rules_raw = json.load(f) @@ -51,39 +53,47 @@ for cam, rules in alert_rules_raw.items(): event_cache = {} + def rule_allows_event(camera, label, zones): cam_key = camera.lower() lbl = label.lower() zones_check = [z.lower() for z in zones] if zones else [] if cam_key not in alert_rules: - logger.debug(f"Camera '{camera}' not in alert_rules.json — event blocked") return False rule = alert_rules[cam_key] - if rule["labels"]: - if lbl not in rule["labels"]: - logger.debug(f"Label '{label}' not allowed for camera '{camera}' — event blocked") - return False - - if rule["ignore"]: - if lbl in rule["ignore"]: - logger.debug(f"Label '{label}' is ignored for camera '{camera}' — event blocked") - return False - + if rule["labels"] and lbl not in rule["labels"]: + return False + if rule["ignore"] and lbl in rule["ignore"]: + return False if rule["zones"]: if not zones_check: - logger.debug(f"No zone info in event but zones filter present — event blocked") return False allowed_zones = [z.lower() for z in rule["zones"]] if not any(zone in allowed_zones for zone in zones_check): - logger.debug(f"Zones {zones} not allowed for camera '{camera}' — event blocked") return False - logger.debug(f"Event allowed for camera '{camera}', label '{label}', zones '{zones}'") return True + +def fetch_snapshot_with_retry(snapshot_url, retries=5, delay=1): + """ + Try to fetch a valid snapshot, retrying if it fails. + """ + for attempt in range(retries): + try: + response = requests.get(snapshot_url, timeout=5) + response.raise_for_status() + if 'image' in response.headers.get('Content-Type', ''): + return response.content + except Exception as e: + logger.debug(f"Snapshot fetch failed (attempt {attempt+1}/{retries}): {e}") + time.sleep(delay) + return None + + def send_email(message, snapshot_urls, event_label, clip_url): subject = f"{event_label} detected!" msg = MIMEMultipart() @@ -95,14 +105,9 @@ def send_email(message, snapshot_urls, event_label, clip_url): msg.attach(MIMEText(body)) for snapshot_url in snapshot_urls: - try: - response = requests.get(snapshot_url, timeout=5) - response.raise_for_status() - if 'image' in response.headers.get('Content-Type', ''): - image_data = BytesIO(response.content) - msg.attach(MIMEImage(image_data.read(), name="snapshot.jpg")) - except Exception: - pass # silently fail snapshot issues + image_bytes = fetch_snapshot_with_retry(snapshot_url) + if image_bytes: + msg.attach(MIMEImage(image_bytes, name="snapshot.jpg")) try: with smtplib.SMTP(SMTP_SERVER, SMTP_PORT, timeout=10) as server: @@ -113,19 +118,26 @@ def send_email(message, snapshot_urls, event_label, clip_url): except Exception as e: logger.error(f"Failed to send email: {e}") -def handle_event(event_id): - time.sleep(10) # Delay to collect snapshots +def handle_event(event_id): if event_id not in event_cache: return event_info = event_cache[event_id] + + # Don’t send again if already emailed + if event_info.get("emailed"): + logger.debug(f"Skipping already emailed event: {event_id}") + return + clip_url = f"{HOMEASSISTANT_URL}/api/frigate/notifications/{event_id}/{event_info['camera']}/clip.mp4" message = f"A {event_info['event_label']} was detected on camera: {event_info['camera']}.\nEvent ID: {event_id}" + send_email(message, event_info['snapshot_urls'], event_info['event_label'], clip_url) + event_cache[event_id]['emailed'] = True logger.info(f"Processed and emailed event: {event_id}") - event_cache.pop(event_id, None) + def on_message(client, userdata, message): try: @@ -151,21 +163,26 @@ def on_message(client, userdata, message): snapshot_url = f"{HOMEASSISTANT_IP}/api/frigate/notifications/{event_id}/snapshot.jpg" - if event_id in event_cache: - event_cache[event_id]['snapshot_urls'].append(snapshot_url) - else: + if event_id not in event_cache: + # First time seeing this event event_cache[event_id] = { 'event_label': event_label, 'camera': camera, - 'snapshot_urls': [snapshot_url] + 'snapshot_urls': [snapshot_url], + 'emailed': False } + # Send email immediately in a thread threading.Thread(target=handle_event, args=(event_id,), daemon=True).start() + else: + # Already seen this event → just collect snapshots + event_cache[event_id]['snapshot_urls'].append(snapshot_url) logger.info(f"Received event: {event_label} from {camera} (Event ID: {event_id}, Zones: {zones})") except Exception as e: logger.error(f"Error processing MQTT message: {e}") + def on_connect(client, userdata, flags, rc, properties=None): if rc != 0: logger.error(f"MQTT connection failed with code {rc}") @@ -178,6 +195,7 @@ def on_connect(client, userdata, flags, rc, properties=None): else: logger.debug("Reconnected to MQTT broker") + def connect_mqtt(): client = mqtt.Client( client_id="frigate_smtp", @@ -199,5 +217,6 @@ def connect_mqtt(): logger.error(f"MQTT connection failed: {e}. Retrying in 5 seconds...") time.sleep(5) + if __name__ == "__main__": connect_mqtt() diff --git a/main.py b/main.py index b58cee5..2a2fa2d 100644 --- a/main.py +++ b/main.py @@ -16,6 +16,7 @@ logging.basicConfig( ) logger = logging.getLogger(__name__) +# Load config with open('config.json', 'r') as f: config = json.load(f) @@ -33,6 +34,7 @@ MQTT_PORT = config["mqtt"]["port"] MQTT_USERNAME = config["mqtt"]["username"] MQTT_PASSWORD = config["mqtt"]["password"] +# Load alert rules try: with open("alert_rules.json", "r") as f: alert_rules_raw = json.load(f) @@ -51,39 +53,47 @@ for cam, rules in alert_rules_raw.items(): event_cache = {} + def rule_allows_event(camera, label, zones): cam_key = camera.lower() lbl = label.lower() zones_check = [z.lower() for z in zones] if zones else [] if cam_key not in alert_rules: - logger.debug(f"Camera '{camera}' not in alert_rules.json — event blocked") return False rule = alert_rules[cam_key] - if rule["labels"]: - if lbl not in rule["labels"]: - logger.debug(f"Label '{label}' not allowed for camera '{camera}' — event blocked") - return False - - if rule["ignore"]: - if lbl in rule["ignore"]: - logger.debug(f"Label '{label}' is ignored for camera '{camera}' — event blocked") - return False - + if rule["labels"] and lbl not in rule["labels"]: + return False + if rule["ignore"] and lbl in rule["ignore"]: + return False if rule["zones"]: if not zones_check: - logger.debug(f"No zone info in event but zones filter present — event blocked") return False allowed_zones = [z.lower() for z in rule["zones"]] if not any(zone in allowed_zones for zone in zones_check): - logger.debug(f"Zones {zones} not allowed for camera '{camera}' — event blocked") return False - logger.debug(f"Event allowed for camera '{camera}', label '{label}', zones '{zones}'") return True + +def fetch_snapshot_with_retry(snapshot_url, retries=5, delay=1): + """ + Try to fetch a valid snapshot, retrying if it fails. + """ + for attempt in range(retries): + try: + response = requests.get(snapshot_url, timeout=5) + response.raise_for_status() + if 'image' in response.headers.get('Content-Type', ''): + return response.content + except Exception as e: + logger.debug(f"Snapshot fetch failed (attempt {attempt+1}/{retries}): {e}") + time.sleep(delay) + return None + + def send_email(message, snapshot_urls, event_label, clip_url): subject = f"{event_label} detected!" msg = MIMEMultipart() @@ -95,14 +105,9 @@ def send_email(message, snapshot_urls, event_label, clip_url): msg.attach(MIMEText(body)) for snapshot_url in snapshot_urls: - try: - response = requests.get(snapshot_url, timeout=5) - response.raise_for_status() - if 'image' in response.headers.get('Content-Type', ''): - image_data = BytesIO(response.content) - msg.attach(MIMEImage(image_data.read(), name="snapshot.jpg")) - except Exception: - pass # silently fail snapshot issues + image_bytes = fetch_snapshot_with_retry(snapshot_url) + if image_bytes: + msg.attach(MIMEImage(image_bytes, name="snapshot.jpg")) try: with smtplib.SMTP(SMTP_SERVER, SMTP_PORT, timeout=10) as server: @@ -113,19 +118,26 @@ def send_email(message, snapshot_urls, event_label, clip_url): except Exception as e: logger.error(f"Failed to send email: {e}") -def handle_event(event_id): - time.sleep(10) # Delay to collect snapshots +def handle_event(event_id): if event_id not in event_cache: return event_info = event_cache[event_id] + + # Don’t send again if already emailed + if event_info.get("emailed"): + logger.debug(f"Skipping already emailed event: {event_id}") + return + clip_url = f"{HOMEASSISTANT_URL}/api/frigate/notifications/{event_id}/{event_info['camera']}/clip.mp4" message = f"A {event_info['event_label']} was detected on camera: {event_info['camera']}.\nEvent ID: {event_id}" + send_email(message, event_info['snapshot_urls'], event_info['event_label'], clip_url) + event_cache[event_id]['emailed'] = True logger.info(f"Processed and emailed event: {event_id}") - event_cache.pop(event_id, None) + def on_message(client, userdata, message): try: @@ -151,21 +163,26 @@ def on_message(client, userdata, message): snapshot_url = f"{HOMEASSISTANT_IP}/api/frigate/notifications/{event_id}/snapshot.jpg" - if event_id in event_cache: - event_cache[event_id]['snapshot_urls'].append(snapshot_url) - else: + if event_id not in event_cache: + # First time seeing this event event_cache[event_id] = { 'event_label': event_label, 'camera': camera, - 'snapshot_urls': [snapshot_url] + 'snapshot_urls': [snapshot_url], + 'emailed': False } + # Send email immediately in a thread threading.Thread(target=handle_event, args=(event_id,), daemon=True).start() + else: + # Already seen this event → just collect snapshots + event_cache[event_id]['snapshot_urls'].append(snapshot_url) logger.info(f"Received event: {event_label} from {camera} (Event ID: {event_id}, Zones: {zones})") except Exception as e: logger.error(f"Error processing MQTT message: {e}") + def on_connect(client, userdata, flags, rc, properties=None): if rc != 0: logger.error(f"MQTT connection failed with code {rc}") @@ -178,6 +195,7 @@ def on_connect(client, userdata, flags, rc, properties=None): else: logger.debug("Reconnected to MQTT broker") + def connect_mqtt(): client = mqtt.Client( client_id="frigate_smtp", @@ -199,5 +217,6 @@ def connect_mqtt(): logger.error(f"MQTT connection failed: {e}. Retrying in 5 seconds...") time.sleep(5) + if __name__ == "__main__": connect_mqtt()