feat: dashboard screenshot upload & retention (last 20 per client)
- Listener: subscribe to dashboard topic, forward screenshots to API - API: store latest + last 20 timestamped screenshots per client, auto-delete older files - Docs: updated README, TECH-CHANGELOG, and copilot-instructions for screenshot upload and retention policy
This commit is contained in:
@@ -2,29 +2,73 @@ import os
|
||||
import json
|
||||
import logging
|
||||
import datetime
|
||||
import base64
|
||||
import requests
|
||||
import paho.mqtt.client as mqtt
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from models.models import Client
|
||||
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(levelname)s] %(message)s')
|
||||
|
||||
# Load .env in development
|
||||
if os.getenv("ENV", "development") == "development":
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(".env")
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(".env")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ENV-abhängige Konfiguration
|
||||
# ENV-dependent configuration
|
||||
ENV = os.getenv("ENV", "development")
|
||||
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO" if ENV == "production" else "DEBUG")
|
||||
DB_URL = os.environ.get(
|
||||
"DB_CONN", "mysql+pymysql://user:password@db/infoscreen")
|
||||
|
||||
# Logging
|
||||
logging.basicConfig(level=logging.DEBUG,
|
||||
format='%(asctime)s [%(levelname)s] %(message)s')
|
||||
DB_URL = os.environ.get("DB_CONN", "mysql+pymysql://user:password@db/infoscreen")
|
||||
|
||||
# DB-Konfiguration
|
||||
engine = create_engine(DB_URL)
|
||||
Session = sessionmaker(bind=engine)
|
||||
|
||||
# API configuration
|
||||
API_BASE_URL = os.getenv("API_BASE_URL", "http://server:8000")
|
||||
|
||||
|
||||
def handle_screenshot(uuid, payload):
|
||||
"""
|
||||
Handle screenshot data received via MQTT and forward to API.
|
||||
Payload can be either raw binary image data or JSON with base64-encoded image.
|
||||
"""
|
||||
try:
|
||||
# Try to parse as JSON first
|
||||
try:
|
||||
data = json.loads(payload.decode())
|
||||
if "image" in data:
|
||||
# Payload is JSON with base64 image
|
||||
api_payload = {"image": data["image"]}
|
||||
headers = {"Content-Type": "application/json"}
|
||||
logging.debug(f"Forwarding base64 screenshot from {uuid} to API")
|
||||
else:
|
||||
logging.warning(f"Screenshot JSON from {uuid} missing 'image' field")
|
||||
return
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
# Payload is raw binary image data - encode to base64 for API
|
||||
image_b64 = base64.b64encode(payload).decode('utf-8')
|
||||
api_payload = {"image": image_b64}
|
||||
headers = {"Content-Type": "application/json"}
|
||||
logging.debug(f"Forwarding binary screenshot from {uuid} to API (encoded as base64)")
|
||||
|
||||
# Forward to API endpoint
|
||||
api_url = f"{API_BASE_URL}/api/clients/{uuid}/screenshot"
|
||||
response = requests.post(api_url, json=api_payload, headers=headers, timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
logging.info(f"Screenshot von {uuid} erfolgreich an API weitergeleitet")
|
||||
else:
|
||||
logging.error(f"API returned status {response.status_code} for screenshot from {uuid}: {response.text}")
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logging.error(f"Failed to forward screenshot from {uuid} to API: {e}")
|
||||
except Exception as e:
|
||||
logging.error(f"Error handling screenshot from {uuid}: {e}")
|
||||
|
||||
|
||||
def on_connect(client, userdata, flags, reasonCode, properties):
|
||||
"""Callback for when client connects or reconnects (API v2)."""
|
||||
@@ -32,7 +76,9 @@ def on_connect(client, userdata, flags, reasonCode, properties):
|
||||
# Subscribe on every (re)connect so we don't miss heartbeats after broker restarts
|
||||
client.subscribe("infoscreen/discovery")
|
||||
client.subscribe("infoscreen/+/heartbeat")
|
||||
logging.info(f"MQTT connected (reasonCode: {reasonCode}); (re)subscribed to discovery and heartbeats")
|
||||
client.subscribe("infoscreen/+/screenshot")
|
||||
client.subscribe("infoscreen/+/dashboard")
|
||||
logging.info(f"MQTT connected (reasonCode: {reasonCode}); (re)subscribed to discovery, heartbeats, screenshots, and dashboards")
|
||||
except Exception as e:
|
||||
logging.error(f"Subscribe failed on connect: {e}")
|
||||
|
||||
@@ -42,6 +88,39 @@ def on_message(client, userdata, msg):
|
||||
logging.debug(f"Empfangene Nachricht auf Topic: {topic}")
|
||||
|
||||
try:
|
||||
# Dashboard-Handling (nested screenshot payload)
|
||||
if topic.startswith("infoscreen/") and topic.endswith("/dashboard"):
|
||||
uuid = topic.split("/")[1]
|
||||
try:
|
||||
payload_text = msg.payload.decode()
|
||||
data = json.loads(payload_text)
|
||||
shot = data.get("screenshot")
|
||||
if isinstance(shot, dict):
|
||||
# Prefer 'data' field (base64) inside screenshot object
|
||||
image_b64 = shot.get("data")
|
||||
if image_b64:
|
||||
logging.debug(f"Dashboard enthält Screenshot für {uuid}; Weiterleitung an API")
|
||||
# Build a lightweight JSON with image field for API handler
|
||||
api_payload = json.dumps({"image": image_b64}).encode("utf-8")
|
||||
handle_screenshot(uuid, api_payload)
|
||||
# Update last_alive if status present
|
||||
if data.get("status") == "alive":
|
||||
session = Session()
|
||||
client_obj = session.query(Client).filter_by(uuid=uuid).first()
|
||||
if client_obj:
|
||||
client_obj.last_alive = datetime.datetime.now(datetime.UTC)
|
||||
session.commit()
|
||||
session.close()
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler beim Verarbeiten des Dashboard-Payloads von {uuid}: {e}")
|
||||
return
|
||||
|
||||
# Screenshot-Handling
|
||||
if topic.startswith("infoscreen/") and topic.endswith("/screenshot"):
|
||||
uuid = topic.split("/")[1]
|
||||
handle_screenshot(uuid, msg.payload)
|
||||
return
|
||||
|
||||
# Heartbeat-Handling
|
||||
if topic.startswith("infoscreen/") and topic.endswith("/heartbeat"):
|
||||
uuid = topic.split("/")[1]
|
||||
|
||||
@@ -2,3 +2,4 @@ paho-mqtt>=2.0
|
||||
SQLAlchemy>=2.0
|
||||
pymysql
|
||||
python-dotenv
|
||||
requests>=2.31.0
|
||||
|
||||
Reference in New Issue
Block a user