Added more filters for zones, cameras, and objects.

This commit is contained in:
Undercover Computing
2025-08-10 17:59:48 +12:00
committed by GitHub
parent 41719c4557
commit 68749e219b
8 changed files with 315 additions and 81 deletions

View File

@ -6,9 +6,9 @@ This repository provides an SMTP service for Frigate, enabling automated email n
## Prerequisites
- [Docker](https://www.docker.com/get-started) installed on your system
- [Docker Compose](https://docs.docker.com/compose/install/) installed
- Git installed to clone the repository
* [Docker](https://www.docker.com/get-started) installed on your system
* [Docker Compose](https://docs.docker.com/compose/install/) installed
* Git installed to clone the repository
## Project Structure
@ -37,6 +37,7 @@ Follow these steps to set up and run the Frigate SMTP service:
Open the `docker-compose.yaml` file in a text editor and define the necessary environment variables. Ensure all required variables (e.g., SMTP server settings) are correctly set.
Example snippet from `docker-compose.yaml`:
```yaml
services:
frigate-smtp:
@ -57,12 +58,45 @@ Follow these steps to set up and run the Frigate SMTP service:
MQTT_PORT: 1883 # MQTT broker port
MQTT_USERNAME: mqttuser # MQTT username for authentication
MQTT_PASSWORD: mqttpass # MQTT password for authentication
ALLOWED_CAMERAS: camera1,camera2 # List of cameras to allow (comma-separated)
IGNORED_LABELS: label1,label2 # Labels to ignore - e.g. car, person, cat; if none, set to "..."
```
Replace `your.smtp.server`, `587`, `your_username`, and `your_password` with your actual SMTP server details.
**Per-camera rules (optional — new feature)**
You can replace the global `ALLOWED_CAMERAS` / `IGNORED_LABELS` approach with a per-camera rules file named `alert_rules.json`. If present, the service will use the rules file instead of the global env vars.
Example `alert_rules.json`:
```json
{
"driveway": {
"labels": ["person", "car"],
"zones": ["Drive"]
},
"backyard": {
"labels": ["person", "dog"],
"zones": []
},
"garage": {
"labels": ["person"],
"zones": ["Front", "Side"]
}
}
```
* **labels**: list of labels to allow for that camera (case-insensitive).
* **zones**: list of zones to allow for that camera (case-insensitive). If left empty or omitted, events in all zones will trigger notifications for that camera.
* If the zones list is non-empty, events detected outside those zones for that camera will **not** generate an email alert.
Mount the file in Docker so the container can read it:
docker-compose.yaml:
```yaml
volumes:
- ./alert_rules.json:/app/alert_rules.json:ro
```
If `alert_rules.json` is present, the script will check the camera name, allowed labels, and allowed zones defined in that file. Cameras not listed in the file will be ignored. If `alert_rules.json` is not present, the script falls back to `ALLOWED_CAMERAS` and `IGNORED_LABELS`.
3. **Run the Application**
Build and start the Docker containers in detached mode:
@ -71,8 +105,8 @@ Follow these steps to set up and run the Frigate SMTP service:
docker compose up --build -d
```
- The `--build` flag ensures the Docker image is built before starting.
- The `-d` flag runs the containers in the background.
* The `--build` flag ensures the Docker image is built before starting.
* The `-d` flag runs the containers in the background.
4. **Verify the Service**
@ -92,11 +126,11 @@ docker compose down
## Troubleshooting
- Ensure all environment variables in `docker-compose.yaml` are correctly set.
- Verify that your SMTP server is accessible and the credentials are valid.
- Check Docker logs for errors: `docker compose logs frigate-smtp`.
- Ensure Docker and Docker Compose are up to date.
* Ensure all environment variables in `docker-compose.yaml` are correctly set.
* Verify that your SMTP server is accessible and the credentials are valid.
* Check Docker logs for errors: `docker compose logs frigate-smtp`.
* Ensure Docker and Docker Compose are up to date.
## Contributing
Contributions are welcome! Please fork the repository, create a new branch, and submit a pull request with your changes.
Contributions are welcome! Please fork the repository, create a new branch, and submit a pull request with your changes.

14
alert_rules.json Normal file
View File

@ -0,0 +1,14 @@
{
"driveway": {
"labels": ["person", "car"],
"zones": ["Drive"]
},
"backyard": {
"labels": ["person", "dog"],
"zones": []
},
"garage": {
"labels": ["person"],
"zones": ["Front", "Side"]
}
}

14
docker/alert_rules.json Normal file
View File

@ -0,0 +1,14 @@
{
"driveway": {
"labels": ["person", "car"],
"zones": ["Drive"]
},
"backyard": {
"labels": ["person", "dog"],
"zones": []
},
"garage": {
"labels": ["person"],
"zones": ["Front", "Side"]
}
}

View File

@ -19,3 +19,5 @@ services:
MQTT_PASSWORD: mqttpass # MQTT password for authentication
ALLOWED_CAMERAS: camera1,camera2 # List of cameras to allow (comma-separated)
IGNORED_LABELS: label1,label2 # Labels to ignore - e.g. car, person, cat; if none, set to "..."
volumes:
- ./alert_rules.json:/app/alert_rules.json:ro

View File

@ -17,10 +17,13 @@ config = {
"port": int(os.getenv("MQTT_PORT", 1883)),
"username": os.getenv("MQTT_USERNAME", ""),
"password": os.getenv("MQTT_PASSWORD", "")
},
"allowed_cameras": os.getenv("ALLOWED_CAMERAS", "").split(",") if os.getenv("ALLOWED_CAMERAS") else [],
"ignored_labels": os.getenv("IGNORED_LABELS", "").split(",") if os.getenv("IGNORED_LABELS") else []
}
}
rules_path = os.getenv("ALERT_RULES_FILE", "alert_rules.json")
if os.path.exists(rules_path):
with open(rules_path, "r") as f:
config["alert_rules"] = json.load(f)
with open("config.json", "w") as f:
json.dump(config, f, indent=2)
json.dump(config, f, indent=2)

View File

@ -10,14 +10,12 @@ import time
import threading
import logging
# Logging setup
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s'
)
logger = logging.getLogger(__name__)
# Load config
with open('config.json', 'r') as f:
config = json.load(f)
@ -28,18 +26,64 @@ SMTP_PASSWORD = config["smtp"]["password"]
EMAIL_FROM = config["smtp"]["from"]
EMAIL_TO = config["smtp"]["to"]
HOMEASSISTANT_URL = config["homeassistant_url"]
HOMEASSISTANT_IP = config["homeassistant_ip"]
HOMEASSISTANT_IP = config.get("homeassistant_ip", HOMEASSISTANT_URL)
MQTT_BROKER_IP = config["mqtt"]["broker_ip"]
MQTT_PORT = config["mqtt"]["port"]
MQTT_USERNAME = config["mqtt"]["username"]
MQTT_PASSWORD = config["mqtt"]["password"]
FILTERED_CAMERAS = config.get("allowed_cameras", [])
IGNORED_LABELS = config.get("ignored_labels", [])
try:
with open("alert_rules.json", "r") as f:
alert_rules_raw = json.load(f)
logger.info(f"Loaded alert_rules.json: {alert_rules_raw}")
except Exception as e:
logger.error(f"Failed to load alert_rules.json, no events will be processed: {e}")
alert_rules_raw = {}
alert_rules = {}
for cam, rules in alert_rules_raw.items():
alert_rules[cam.lower()] = {
"labels": [lbl.lower() for lbl in rules.get("labels", [])],
"ignore": [lbl.lower() for lbl in rules.get("ignore", [])],
"zones": [zone.lower() for zone in rules.get("zones", [])]
}
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["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 send_email(message, snapshot_urls, event_label, clip_url):
subject = f"{event_label} detected!"
msg = MIMEMultipart()
@ -58,7 +102,7 @@ def send_email(message, snapshot_urls, event_label, clip_url):
image_data = BytesIO(response.content)
msg.attach(MIMEImage(image_data.read(), name="snapshot.jpg"))
except Exception:
pass # silent fail for snapshot issues
pass # silently fail snapshot issues
try:
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT, timeout=10) as server:
@ -70,7 +114,7 @@ def send_email(message, snapshot_urls, event_label, clip_url):
logger.error(f"Failed to send email: {e}")
def handle_event(event_id):
time.sleep(10) # Delay to collect full snapshots
time.sleep(10) # Delay to collect snapshots
if event_id not in event_cache:
return
@ -93,17 +137,16 @@ def on_message(client, userdata, message):
if not after:
return
event_id = after.get("id")
event_label = after.get("label")
event_id = after.get("id")
camera = after.get("camera")
zones = after.get("current_zones") or after.get("entered_zones") or []
if not event_id or not event_label or not camera:
if not event_label or not event_id or not camera:
return
if FILTERED_CAMERAS and camera.lower() not in [c.lower() for c in FILTERED_CAMERAS]:
return
if event_label in IGNORED_LABELS:
if not rule_allows_event(camera, event_label, zones):
logger.info(f"Event from camera '{camera}' with label '{event_label}' and zones '{zones}' blocked by alert rules.")
return
snapshot_url = f"{HOMEASSISTANT_IP}/api/frigate/notifications/{event_id}/snapshot.jpg"
@ -118,7 +161,7 @@ def on_message(client, userdata, message):
}
threading.Thread(target=handle_event, args=(event_id,), daemon=True).start()
logger.info(f"Received event: {event_label} from {camera} (Event ID: {event_id})")
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}")

138
log.py
View File

@ -1,4 +1,3 @@
# Import necessary dependencies.
import paho.mqtt.client as mqtt
import smtplib
from email.mime.multipart import MIMEMultipart
@ -11,7 +10,6 @@ from io import BytesIO
import time
import threading
# Set up logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s',
@ -21,7 +19,6 @@ logging.basicConfig(
]
)
# Load settings from config.json
with open('config.json', 'r') as f:
config = json.load(f)
@ -32,17 +29,64 @@ SMTP_PASSWORD = config["smtp"]["password"]
EMAIL_FROM = config["smtp"]["from"]
EMAIL_TO = config["smtp"]["to"]
HOMEASSISTANT_URL = config["homeassistant_url"]
HOMEASSISTANT_IP = config.get("homeassistant_ip", HOMEASSISTANT_URL)
# MQTT configuration
MQTT_BROKER_IP = config["mqtt"]["broker_ip"]
MQTT_PORT = config["mqtt"]["port"]
MQTT_USERNAME = config["mqtt"]["username"]
MQTT_PASSWORD = config["mqtt"]["password"]
# Dictionary to track event IDs and email state
try:
with open("alert_rules.json", "r") as f:
alert_rules_raw = json.load(f)
logging.info(f"Loaded alert_rules.json: {alert_rules_raw}")
except Exception as e:
logging.error(f"Failed to load alert_rules.json, no events will be processed: {e}")
alert_rules_raw = {}
alert_rules = {}
for cam, rules in alert_rules_raw.items():
alert_rules[cam.lower()] = {
"labels": [lbl.lower() for lbl in rules.get("labels", [])],
"ignore": [lbl.lower() for lbl in rules.get("ignore", [])],
"zones": [zone.lower() for zone in rules.get("zones", [])]
}
event_cache = {}
# Function to send email with attachment and clip link
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:
logging.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"]:
logging.debug(f"Label '{label}' not allowed for camera '{camera}' — event blocked")
return False
if rule["ignore"]:
if lbl in rule["ignore"]:
logging.debug(f"Label '{label}' is ignored for camera '{camera}' — event blocked")
return False
if rule["zones"]:
if not zones_check:
logging.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):
logging.debug(f"Zones {zones} not allowed for camera '{camera}' — event blocked")
return False
logging.debug(f"Event allowed for camera '{camera}', label '{label}', zones '{zones}'")
return True
def send_email(message, snapshot_urls, event_label, clip_url):
try:
subject = f"(Test) {event_label} detected!"
@ -51,19 +95,20 @@ def send_email(message, snapshot_urls, event_label, clip_url):
msg['From'] = EMAIL_FROM
msg['To'] = ", ".join(EMAIL_TO)
# Add the email body
body = f"{message}\n\nClip: {clip_url}"
msg.attach(MIMEText(body))
# Attach snapshots to the email
for snapshot_url in snapshot_urls:
response = requests.get(snapshot_url)
response.raise_for_status()
image_data = BytesIO(response.content)
msg.attach(MIMEImage(image_data.read(), name="snapshot.jpg"))
try:
logging.debug(f"Fetching snapshot: {snapshot_url}")
response = requests.get(snapshot_url, timeout=5)
response.raise_for_status()
image_data = BytesIO(response.content)
msg.attach(MIMEImage(image_data.read(), name="snapshot.jpg"))
except Exception as e:
logging.warning(f"Failed to fetch or attach snapshot {snapshot_url}: {e}")
# Send the email
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT, timeout=10) as server:
server.starttls()
server.login(SMTP_USERNAME, SMTP_PASSWORD)
server.sendmail(EMAIL_FROM, EMAIL_TO, msg.as_string())
@ -72,42 +117,71 @@ def send_email(message, snapshot_urls, event_label, clip_url):
except Exception as e:
logging.error(f"Failed to send email: {e}")
# Function to handle the event timeout and send email after waiting for new snapshots
def handle_event(event_id, event_label, snapshot_urls):
time.sleep(7.5) # Wait for more snapshots before sending email
def handle_event(event_id):
logging.debug(f"Event handler started for event ID: {event_id}")
time.sleep(7.5) # Delay to allow snapshots to accumulate
clip_url = f"{HOMEASSISTANT_URL}/api/frigate/notifications/{event_id}/gate/clip.mp4"
email_message = f"A {event_label} was detected.\nEvent ID: {event_id}"
send_email(email_message, snapshot_urls, event_label, clip_url)
event_info = event_cache.get(event_id)
if not event_info:
logging.warning(f"No cached info found for event ID: {event_id} on handle_event")
return
clip_url = f"{HOMEASSISTANT_URL}/api/frigate/notifications/{event_id}/{event_info['camera']}/clip.mp4"
email_message = f"A {event_info['event_label']} was detected on camera {event_info['camera']}.\nEvent ID: {event_id}"
send_email(email_message, event_info['snapshot_urls'], event_info['event_label'], clip_url)
# Remove event from cache after processing
event_cache.pop(event_id, None)
logging.debug(f"Event ID {event_id} processed and removed from cache")
# MQTT message callback
def on_message(client, userdata, message):
try:
event_data = json.loads(message.payload.decode("utf-8"))
logging.debug(f"Frigate event data: {event_data}")
logging.debug(f"Received MQTT message: {event_data}")
event_label = event_data["after"]["label"]
event_id = event_data["after"]["id"]
snapshot_url = f"{HOMEASSISTANT_URL}/api/frigate/notifications/{event_id}/snapshot.jpg"
if event_data.get("type") != "new":
logging.debug("Event type is not 'new', ignoring.")
return
after = event_data.get("after")
if not after:
logging.debug("No 'after' data in event, ignoring.")
return
event_label = after.get("label")
event_id = after.get("id")
camera = after.get("camera")
# Use correct zone info as list
zones = after.get("current_zones") or after.get("entered_zones") or []
if not event_label or not event_id or not camera:
logging.debug("Missing label, id or camera in event, ignoring.")
return
if not rule_allows_event(camera, event_label, zones):
logging.info(f"Event from camera '{camera}' with label '{event_label}' and zones '{zones}' blocked by alert rules.")
return
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)
logging.debug(f"Added snapshot to existing event cache: {event_id}")
else:
event_cache[event_id] = {
'event_label': event_label,
'camera': camera,
'snapshot_urls': [snapshot_url],
'timer': threading.Thread(target=handle_event, args=(event_id, event_label, [snapshot_url]))
'timer': threading.Thread(target=handle_event, args=(event_id,))
}
event_cache[event_id]['timer'].start()
logging.debug(f"Started new event handler thread for event ID: {event_id}")
logging.info(f"Event processed: {event_label} - Event ID: {event_id} from camera: {camera} Zones: {zones}")
logging.info(f"Event processed: {event_label} - Event ID: {event_id}")
except Exception as e:
logging.error(f"Error processing MQTT message: {e}")
# MQTT connection callback
def on_connect(client, userdata, flags, rc):
if rc == 0:
logging.info("Connected to MQTT broker successfully")
@ -115,7 +189,6 @@ def on_connect(client, userdata, flags, rc):
else:
logging.error(f"Failed to connect to MQTT broker. Return code: {rc}")
# MQTT setup
def connect_mqtt():
client = mqtt.Client()
client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
@ -130,12 +203,11 @@ def connect_mqtt():
logging.error(f"Error during MQTT connection: {e}")
if __name__ == "__main__":
# Print or log the warning message when the script is run
print("WARNING: USE THIS FOR TESTING AND DEBUGGING ONLY!")
logging.warning("WARNING: USE THIS FOR TESTING AND DEBUGGING ONLY!")
time.sleep(2)
print("WARNING: USE THIS FOR TESTING AND DEBUGGING ONLY!")
logging.warning("WARNING: USE THIS FOR TESTING AND DEBUGGING ONLY!")
logging.info("Starting Frigate Event Notifier...")
connect_mqtt()
connect_mqtt()

88
main.py
View File

@ -10,14 +10,12 @@ import time
import threading
import logging
# Logging setup
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s'
)
logger = logging.getLogger(__name__)
# Load config
with open('config.json', 'r') as f:
config = json.load(f)
@ -28,18 +26,64 @@ SMTP_PASSWORD = config["smtp"]["password"]
EMAIL_FROM = config["smtp"]["from"]
EMAIL_TO = config["smtp"]["to"]
HOMEASSISTANT_URL = config["homeassistant_url"]
HOMEASSISTANT_IP = config["homeassistant_ip"]
HOMEASSISTANT_IP = config.get("homeassistant_ip", HOMEASSISTANT_URL)
MQTT_BROKER_IP = config["mqtt"]["broker_ip"]
MQTT_PORT = config["mqtt"]["port"]
MQTT_USERNAME = config["mqtt"]["username"]
MQTT_PASSWORD = config["mqtt"]["password"]
FILTERED_CAMERAS = config.get("allowed_cameras", [])
IGNORED_LABELS = config.get("ignored_labels", [])
try:
with open("alert_rules.json", "r") as f:
alert_rules_raw = json.load(f)
logger.info(f"Loaded alert_rules.json: {alert_rules_raw}")
except Exception as e:
logger.error(f"Failed to load alert_rules.json, no events will be processed: {e}")
alert_rules_raw = {}
alert_rules = {}
for cam, rules in alert_rules_raw.items():
alert_rules[cam.lower()] = {
"labels": [lbl.lower() for lbl in rules.get("labels", [])],
"ignore": [lbl.lower() for lbl in rules.get("ignore", [])],
"zones": [zone.lower() for zone in rules.get("zones", [])]
}
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["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 send_email(message, snapshot_urls, event_label, clip_url):
subject = f"{event_label} detected!"
msg = MIMEMultipart()
@ -58,7 +102,7 @@ def send_email(message, snapshot_urls, event_label, clip_url):
image_data = BytesIO(response.content)
msg.attach(MIMEImage(image_data.read(), name="snapshot.jpg"))
except Exception:
pass # silent fail for snapshot issues
pass # silently fail snapshot issues
try:
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT, timeout=10) as server:
@ -70,7 +114,7 @@ def send_email(message, snapshot_urls, event_label, clip_url):
logger.error(f"Failed to send email: {e}")
def handle_event(event_id):
time.sleep(10) # Delay to collect full snapshots
time.sleep(10) # Delay to collect snapshots
if event_id not in event_cache:
return
@ -93,17 +137,16 @@ def on_message(client, userdata, message):
if not after:
return
event_id = after.get("id")
event_label = after.get("label")
event_id = after.get("id")
camera = after.get("camera")
zones = after.get("current_zones") or after.get("entered_zones") or []
if not event_id or not event_label or not camera:
if not event_label or not event_id or not camera:
return
if FILTERED_CAMERAS and camera.lower() not in [c.lower() for c in FILTERED_CAMERAS]:
return
if event_label in IGNORED_LABELS:
if not rule_allows_event(camera, event_label, zones):
logger.info(f"Event from camera '{camera}' with label '{event_label}' and zones '{zones}' blocked by alert rules.")
return
snapshot_url = f"{HOMEASSISTANT_IP}/api/frigate/notifications/{event_id}/snapshot.jpg"
@ -118,20 +161,29 @@ def on_message(client, userdata, message):
}
threading.Thread(target=handle_event, args=(event_id,), daemon=True).start()
logger.info(f"Received event: {event_label} from {camera} (Event ID: {event_id})")
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):
if rc == 0:
def on_connect(client, userdata, flags, rc, properties=None):
if rc != 0:
logger.error(f"MQTT connection failed with code {rc}")
return
if userdata.get("first_connect", True):
client.subscribe("frigate/events")
logger.info("Connected to MQTT broker and subscribed to frigate/events")
userdata["first_connect"] = False
else:
logger.error(f"MQTT connection failed with code {rc}")
logger.debug("Reconnected to MQTT broker")
def connect_mqtt():
client = mqtt.Client(client_id="frigate_smtp", protocol=mqtt.MQTTv311)
client = mqtt.Client(
client_id="frigate_smtp",
protocol=mqtt.MQTTv5,
userdata={"first_connect": True}
)
client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
client.on_connect = on_connect
client.on_message = on_message