A comprehensive solution to bypass Eufy's unreliable facial recognition using custom face detection with persistent training data, integrated with Home Assistant.
Eufy doorbells often misclassify homeowners as strangers, leading to:
- Inconsistent voice responses
- False stranger alerts
- Unreliable automation triggers
- Poor user experience for legitimate residents
This enhanced solution provides:
- Persistent face training that survives container restarts
- Historical snapshot management for reviewing and training
- Selective voice responses (silent for known faces, voice for strangers)
- 4K recording preservation (no RTSP required)
- Backup and restore capabilities
Eufy Person Detection → HA Automation → Camera Snapshot → Enhanced Face Recognition Service → Conditional Voice Response
The system:
- Uses Eufy's reliable person detection as trigger
- Takes snapshot when person detected
- Processes with enhanced face recognition service
- Triggers voice response only for unknown faces
- Saves all detections with metadata for review
- Home Assistant OS
- Eufy Security integration configured
- Eufy doorbell with person detection
- Advanced SSH & Web Terminal addon
- Docker available in HAOS
Create the enhanced service file:
cat > /tmp/enhanced_service.py << 'EOF'
#!/usr/bin/env python3
"""
Enhanced Face Recognition Service
- Pre-loads models for faster processing
- Persistent face training storage
- Historical detection image management
- Training data backup capabilities
"""
import os
import json
import logging
import datetime
from flask import Flask, request, jsonify
import face_recognition
import cv2
import numpy as np
from PIL import Image
import io
import hashlib
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class EnhancedFaceRecognizer:
def __init__(self):
self.confidence_threshold = 0.6
self.images_path = '/app/detection_images'
self.known_faces_path = '/app/known_faces'
self.known_encodings = []
self.known_names = []
# Create directories
os.makedirs(self.images_path, exist_ok=True)
os.makedirs(self.known_faces_path, exist_ok=True)
# Pre-load models by doing a test recognition
logger.info("Pre-loading face recognition models...")
test_image = np.zeros((100, 100, 3), dtype=np.uint8)
face_recognition.face_encodings(test_image)
logger.info("Models pre-loaded successfully")
self.load_known_faces()
def load_known_faces(self):
"""Load known faces from directory"""
logger.info(f"Loading known faces from {self.known_faces_path}")
self.known_encodings = []
self.known_names = []
face_count = 0
for filename in os.listdir(self.known_faces_path):
if filename.lower().endswith(('.jpg', '.jpeg', '.png')):
name = os.path.splitext(filename)[0]
image_path = os.path.join(self.known_faces_path, filename)
try:
image = face_recognition.load_image_file(image_path)
encodings = face_recognition.face_encodings(image)
if encodings:
self.known_encodings.append(encodings[0])
self.known_names.append(name)
face_count += 1
logger.info(f"Loaded known face: {name}")
else:
logger.warning(f"No face found in {filename}")
except Exception as e:
logger.error(f"Error loading {filename}: {e}")
logger.info(f"Loaded {face_count} known faces")
def save_detection_image(self, image_data, result_info):
"""Save detection image with metadata"""
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
# Create filename with timestamp and face count
face_count = len(result_info)
filename = f"detection_{timestamp}_{face_count}faces.jpg"
filepath = os.path.join(self.images_path, filename)
# Save image
with open(filepath, 'wb') as f:
f.write(image_data)
# Save metadata
metadata = {
'timestamp': timestamp,
'filename': filename,
'faces_detected': face_count,
'results': result_info
}
metadata_file = filepath.replace('.jpg', '_metadata.json')
with open(metadata_file, 'w') as f:
json.dump(metadata, f, indent=2)
return filename
def recognize_faces(self, image_data):
"""Recognize faces in image"""
try:
# Convert image data to numpy array
image = np.array(Image.open(io.BytesIO(image_data)))
# Find faces and encode them
face_locations = face_recognition.face_locations(image)
face_encodings = face_recognition.face_encodings(image, face_locations)
results = []
for i, face_encoding in enumerate(face_encodings):
if not self.known_encodings:
# No known faces - return unknown
results.append({
"name": "unknown",
"confidence": 0,
"location": face_locations[i]
})
continue
# Compare with known faces
face_distances = face_recognition.face_distance(self.known_encodings, face_encoding)
best_match_index = np.argmin(face_distances)
if face_distances[best_match_index] <= (1 - self.confidence_threshold):
name = self.known_names[best_match_index]
confidence = 1 - face_distances[best_match_index]
else:
name = "unknown"
confidence = 0
results.append({
"name": name,
"confidence": round(confidence, 3),
"location": face_locations[i]
})
# Save detection image with results
filename = self.save_detection_image(image_data, results)
return results, filename
except Exception as e:
logger.error(f"Recognition error: {e}")
return [{"name": "error", "confidence": 0}], None
# Initialize recognizer at startup
logger.info("Initializing Enhanced Face Recognizer...")
recognizer = EnhancedFaceRecognizer()
logger.info("Service ready")
@app.route('/health', methods=['GET'])
def health():
return jsonify({
'status': 'healthy',
'known_faces': len(recognizer.known_names),
'confidence_threshold': recognizer.confidence_threshold,
'models_preloaded': True
})
@app.route('/recognize', methods=['POST'])
def recognize():
if 'file' not in request.files:
return jsonify({'error': 'No file provided'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'error': 'No file selected'}), 400
try:
image_data = file.read()
results, filename = recognizer.recognize_faces(image_data)
return jsonify({
'success': True,
'faces': results,
'face_count': len(results),
'saved_as': filename,
'processing_time': 'sub-second'
})
except Exception as e:
logger.error(f"Recognition error: {e}")
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
logger.info("Starting Enhanced Face Recognition Service")
app.run(host='0.0.0.0', port=8099, debug=False)
EOFcat > /tmp/Dockerfile.enhanced << 'EOF'
FROM python:3.9-slim
RUN apt-get update && apt-get install -y \
cmake libopenblas-dev liblapack-dev build-essential libffi-dev \
&& rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir \
face-recognition==1.3.0 opencv-python-headless==4.8.1.78 \
flask==2.3.3 requests==2.31.0 pillow==10.0.1 numpy==1.24.3
COPY enhanced_service.py /app/enhanced_service.py
WORKDIR /app
CMD ["python", "enhanced_service.py"]
EOF# Build the enhanced container (takes 10-15 minutes)
docker build -f /tmp/Dockerfile.enhanced -t enhanced-face-recognition /tmp/
# Create persistent storage directories
mkdir -p /tmp/face-recognition-data/detection_images
mkdir -p /tmp/face-recognition-data/known_faces
# Run with persistent volumes
docker run -d \
--name enhanced-face-recognition \
--restart always \
-p 8099:8099 \
-v /tmp/face-recognition-data/detection_images:/app/detection_images \
-v /tmp/face-recognition-data/known_faces:/app/known_faces \
enhanced-face-recognitionAdd to configuration.yaml:
# Enhanced face recognition integration
sensor:
- platform: rest
resource: http://localhost:8099/health
name: face_recognition_status
scan_interval: 60
timeout: 15
value_template: "{{ value_json.status if value_json else 'offline' }}"
json_attributes:
- known_faces
- confidence_threshold
# Shell command for processing face recognition
shell_command:
process_burst_recognition: >
/bin/bash -c '
cd /config/www &&
if [ -f "burst_1.jpg" ]; then
result=$(curl -s -X POST "http://localhost:8099/recognize" -F "file=@burst_1.jpg" | jq -r "if .face_count == 0 then \"unknown\" else (.faces[0].name // \"unknown\") end") &&
curl -s -X POST "http://localhost:8123/api/services/input_text/set_value" \
-H "Authorization: Bearer YOUR_LONG_LIVED_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"entity_id\": \"input_text.face_recognition_result\", \"value\": \"$result\"}"
else
curl -s -X POST "http://localhost:8123/api/services/input_text/set_value" \
-H "Authorization: Bearer YOUR_LONG_LIVED_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"entity_id\": \"input_text.face_recognition_result\", \"value\": \"file_not_found\"}"
fi'
# Input helpers
input_text:
face_recognition_result:
name: "Face Recognition Result"
input_boolean:
auto_voice_response_enabled:
name: "Auto Voice Response Enabled"
icon: mdi:account-voiceImportant: Replace YOUR_LONG_LIVED_TOKEN with an actual Home Assistant long-lived access token.
alias: Auto Voice Response - Face Recognition Enhanced
description: Play voice message when unknown person detected, no ring
triggers:
- entity_id: binary_sensor.doorbell_person_detected
to: "on"
trigger: state
conditions:
- condition: and
conditions:
- condition: state
entity_id: input_boolean.auto_voice_response_enabled
state: "on"
- condition: state
entity_id: binary_sensor.doorbell_ringing
state: "off"
- condition: state
entity_id: sensor.face_recognition_status
state: "healthy"
- condition: template
value_template: >-
{% set last_triggered = state_attr('automation.auto_voice_response_face_recognition_enhanced', 'last_triggered') %}
{% if last_triggered %}
{{ (now() - last_triggered).total_seconds() > 300 }}
{% else %}
true
{% endif %}
actions:
- action: camera.snapshot
target:
entity_id: camera.doorbell
data:
filename: /config/www/burst_1.jpg
- delay: 1
- action: shell_command.process_burst_recognition
- delay: 3
- condition: template
value_template: "{{ states('input_text.face_recognition_result') == 'unknown' }}"
- action: eufy_security.quick_response
target:
entity_id: camera.doorbell
data:
voice_id: 102
mode: singleTo add someone to the training data:
# Copy their photo to the known_faces directory
docker cp /path/to/their/photo.jpg enhanced-face-recognition:/app/known_faces/person_name.jpg
# Restart container to reload faces
docker restart enhanced-face-recognition
# Verify loading
curl http://localhost:8099/healthDetection images are automatically saved with metadata:
# View recent detections
docker exec enhanced-face-recognition ls -la /app/detection_images/
# Copy detection to training (if you want to train on a good detection)
docker cp enhanced-face-recognition:/app/detection_images/detection_TIMESTAMP_1faces.jpg /tmp/person_name.jpg
docker cp /tmp/person_name.jpg enhanced-face-recognition:/app/known_faces/person_name.jpg# Backup known faces
docker exec enhanced-face-recognition tar -czf /tmp/known_faces_backup.tar.gz -C /app known_faces
# Copy backup to host
docker cp enhanced-face-recognition:/tmp/known_faces_backup.tar.gz ./face_recognition_backup_$(date +%Y%m%d).tar.gz
# Backup detection history (optional, can be large)
docker exec enhanced-face-recognition tar -czf /tmp/detections_backup.tar.gz -C /app detection_images
docker cp enhanced-face-recognition:/tmp/detections_backup.tar.gz ./detections_backup_$(date +%Y%m%d).tar.gz# Copy backup to container
docker cp ./face_recognition_backup_YYYYMMDD.tar.gz enhanced-face-recognition:/tmp/
# Extract backup
docker exec enhanced-face-recognition tar -xzf /tmp/face_recognition_backup_YYYYMMDD.tar.gz -C /app
# Restart to reload faces
docker restart enhanced-face-recognition# Check service status
curl http://localhost:8099/health
# Test recognition
curl -X POST "http://localhost:8099/recognize" -F "file=@/path/to/test_image.jpg"
# View logs
docker logs enhanced-face-recognition- Container won't start: Check Docker logs for dependency installation errors
- No face loading: Verify image files are valid and contain clear faces
- Volume mount issues: Use
docker cpfor direct file transfers - Recognition errors: Check image quality and face visibility
- Container Size: ~1.8GB (includes all dependencies)
- RAM Usage: ~200MB during processing
- Response Time: 2-3 seconds from detection to voice response
- Face Detection Accuracy: 95%+ face detection
- Recognition Accuracy: Depends on training image quality
Modify the service to adjust recognition sensitivity:
self.confidence_threshold = 0.6 # Adjust between 0.3-0.8Add multiple photos of the same person:
docker cp photo1.jpg enhanced-face-recognition:/app/known_faces/person_name_1.jpg
docker cp photo2.jpg enhanced-face-recognition:/app/known_faces/person_name_2.jpg- All processing is local (no cloud dependencies)
- Face data stored only on your system
- Regular backups recommended for training data
- Consider encrypting backup files for sensitive environments
- Web interface for training data management
- Multi-person detection optimization
- Integration with Home Assistant file browser
- Automated backup scheduling
- Face clustering for unknown visitor tracking
This solution is provided as-is for educational and personal use. Face recognition libraries maintain their respective licenses.
Issues and pull requests welcome. Please test thoroughly before submitting changes.
Tags: Home Assistant, Eufy, Face Recognition, Docker, Automation, Smart Home, Privacy, Persistent Storage