feat: add SocketIO server integration
- Initialize SocketIO with threading mode - Add WebSocket event handlers (connect, disconnect, subscribe_floor) - Add broadcast_scan_update() function for pushing scan results - Integrate broadcast with BT scan endpoint - Update run_server() to use socketio.run() Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,8 @@ import time
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from flask import Flask, jsonify, render_template, request
|
from flask import Flask, current_app, jsonify, render_template, request
|
||||||
|
from flask_socketio import SocketIO, emit
|
||||||
|
|
||||||
from ..scanner import RFScanner
|
from ..scanner import RFScanner
|
||||||
from ..distance import estimate_distance
|
from ..distance import estimate_distance
|
||||||
@@ -16,6 +17,29 @@ from ..bluetooth_identify import identify_single_device, identify_device
|
|||||||
from ..database import DeviceDatabase, init_database, get_database
|
from ..database import DeviceDatabase, init_database, get_database
|
||||||
from ..homeassistant import HAWebhooks, HAWebhookConfig
|
from ..homeassistant import HAWebhooks, HAWebhookConfig
|
||||||
|
|
||||||
|
# Module-level SocketIO instance
|
||||||
|
socketio = SocketIO()
|
||||||
|
|
||||||
|
|
||||||
|
def broadcast_scan_update(app: Flask, devices: list[dict], scan_type: str = "bluetooth"):
|
||||||
|
"""Broadcast scan results to all connected WebSocket clients."""
|
||||||
|
sio = app.config.get("SOCKETIO")
|
||||||
|
if not sio:
|
||||||
|
return
|
||||||
|
|
||||||
|
scanner_identity = app.config.get("SCANNER_IDENTITY", {})
|
||||||
|
|
||||||
|
sio.emit(
|
||||||
|
"scan_update",
|
||||||
|
{
|
||||||
|
"type": scan_type,
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
"scanner_id": scanner_identity.get("id", "unknown"),
|
||||||
|
"devices": devices,
|
||||||
|
},
|
||||||
|
namespace="/ws/scan",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AutoScanner:
|
class AutoScanner:
|
||||||
"""Background scanner that runs periodic scans"""
|
"""Background scanner that runs periodic scans"""
|
||||||
@@ -213,6 +237,10 @@ def create_app(config: Config | None = None) -> Flask:
|
|||||||
# Store config reference
|
# Store config reference
|
||||||
app.config["RF_CONFIG"] = config
|
app.config["RF_CONFIG"] = config
|
||||||
|
|
||||||
|
# Initialize SocketIO with threading mode (compatible with existing threads)
|
||||||
|
socketio.init_app(app, cors_allowed_origins="*", async_mode="threading")
|
||||||
|
app.config["SOCKETIO"] = socketio
|
||||||
|
|
||||||
# Data directory from config
|
# Data directory from config
|
||||||
app.config["DATA_DIR"] = config.get_data_dir()
|
app.config["DATA_DIR"] = config.get_data_dir()
|
||||||
app.config["DATA_DIR"].mkdir(parents=True, exist_ok=True)
|
app.config["DATA_DIR"].mkdir(parents=True, exist_ok=True)
|
||||||
@@ -320,6 +348,31 @@ def create_app(config: Config | None = None) -> Flask:
|
|||||||
location_label=config.auto_scan.location_label
|
location_label=config.auto_scan.location_label
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ==================== WebSocket Event Handlers ====================
|
||||||
|
|
||||||
|
@socketio.on("connect", namespace="/ws/scan")
|
||||||
|
def ws_connect():
|
||||||
|
"""Handle client connection."""
|
||||||
|
scanner_id = current_app.config.get("SCANNER_IDENTITY", {}).get("id", "unknown")
|
||||||
|
emit("connected", {"scanner_id": scanner_id})
|
||||||
|
print(f"[WS] Client connected: {request.sid}")
|
||||||
|
|
||||||
|
@socketio.on("disconnect", namespace="/ws/scan")
|
||||||
|
def ws_disconnect():
|
||||||
|
"""Handle client disconnection."""
|
||||||
|
print(f"[WS] Client disconnected: {request.sid}")
|
||||||
|
|
||||||
|
@socketio.on("subscribe_floor", namespace="/ws/scan")
|
||||||
|
def ws_subscribe_floor(data):
|
||||||
|
"""Subscribe to floor-specific updates."""
|
||||||
|
from flask_socketio import join_room
|
||||||
|
|
||||||
|
floor = data.get("floor", "all")
|
||||||
|
join_room(f"floor_{floor}")
|
||||||
|
emit("subscribed", {"floor": floor})
|
||||||
|
|
||||||
|
# ==================== HTTP Routes ====================
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
"""Main dashboard page"""
|
"""Main dashboard page"""
|
||||||
@@ -1072,6 +1125,9 @@ def create_app(config: Config | None = None) -> Flask:
|
|||||||
scan_type="bluetooth"
|
scan_type="bluetooth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Broadcast to WebSocket clients
|
||||||
|
broadcast_scan_update(current_app, response_data["bluetooth_devices"], "bluetooth")
|
||||||
|
|
||||||
return jsonify(response_data)
|
return jsonify(response_data)
|
||||||
|
|
||||||
# ==================== Historical Data API ====================
|
# ==================== Historical Data API ====================
|
||||||
@@ -1476,10 +1532,12 @@ def run_server(
|
|||||||
if log_requests:
|
if log_requests:
|
||||||
print(f"Request logging: ENABLED")
|
print(f"Request logging: ENABLED")
|
||||||
print(f"Log output: {config.get_data_dir() / 'logs'}")
|
print(f"Log output: {config.get_data_dir() / 'logs'}")
|
||||||
|
print(f"WebSocket: ENABLED (namespace /ws/scan)")
|
||||||
print(f"{'='*60}")
|
print(f"{'='*60}")
|
||||||
print(f"Server running at: http://{host}:{port}")
|
print(f"Server running at: http://{host}:{port}")
|
||||||
print(f"Local access: http://localhost:{port}")
|
print(f"Local access: http://localhost:{port}")
|
||||||
print(f"Network access: http://<your-ip>:{port}")
|
print(f"Network access: http://<your-ip>:{port}")
|
||||||
print(f"{'='*60}\n")
|
print(f"{'='*60}\n")
|
||||||
|
|
||||||
app.run(host=host, port=port, debug=debug)
|
# Use socketio.run() for WebSocket support
|
||||||
|
socketio.run(app, host=host, port=port, debug=debug, allow_unsafe_werkzeug=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user