Skip to content

Realtime Guide

Django DeviceHub can push live device events to web clients via WebSocket (Django Channels) or Server-Sent Events (SSE). This guide covers both backends, the bridge architecture, and frontend integration.

Architecture

The realtime system has three layers:

  1. Signals -- device_data_received and device_status_changed fire when the MessageRouter processes MQTT messages
  2. Bridge -- RealtimeBridge listens to signals and broadcasts events to the configured backend
  3. Transport -- Either Django Channels (WebSocket) or SSE delivers events to connected web clients
MQTT Broker
    |
    v
MessageRouter  -->  Django Signal  -->  RealtimeBridge
                                              |
                         +--------------------+--------------------+
                         |                                         |
                   Channels backend                           SSE backend
                   (channel layer)                         (in-process queues)
                         |                                         |
                   WebSocket consumer                        SSE streaming view
                         |                                         |
                      Browser                                   Browser

The bridge is a module-level singleton that auto-connects during app startup:

from django_devicehub.realtime.bridge import realtime_bridge

Configuration

Choose a backend in your settings:

DJANGO_DEVICEHUB = {
    "REALTIME": {
        "BACKEND": "channels",   # or "sse"
    },
}
Backend Requirements Pros Cons
channels Django Channels + Redis Bidirectional, scales across processes Requires ASGI server + Redis
sse None (built-in) Zero dependencies, works with WSGI Unidirectional, single-process only

WebSocket Backend (Django Channels)

Installation

pip install django-devicehub[channels]

This installs Django Channels and channels-redis.

Django settings

INSTALLED_APPS = [
    "daphne",          # or uvicorn
    "channels",
    "django_devicehub",
    ...
]

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [("localhost", 6379)],
        },
    },
}

DJANGO_DEVICEHUB = {
    "REALTIME": {
        "BACKEND": "channels",
    },
}

ASGI routing

Add WebSocket URL patterns to your ASGI application:

# asgi.py
import os
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from django.core.asgi import get_asgi_application
from django_devicehub.realtime.routing import websocket_urlpatterns

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")

application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    "websocket": AuthMiddlewareStack(
        URLRouter(websocket_urlpatterns)
    ),
})

WebSocket URL patterns

Two URL patterns are registered:

Pattern Description
ws://host/ws/iot/{device_type}/ All devices of a type
ws://host/ws/iot/{device_type}/{device_id}/ Specific device

DeviceConsumer

The DeviceConsumer is a JsonWebSocketConsumer that:

  • Rejects anonymous connections with close code 4403
  • Joins channel groups based on the URL: iot_{device_type} and optionally iot_{device_type}_{device_id}
  • Forwards events from the bridge to the client as JSON messages
  • Supports ping/pong: send {"action": "ping"} and receive {"event": "pong"}

Messages sent to the client have this shape:

{
    "event": "data",
    "device_type": "weatherstation",
    "device_id": "ws-001",
    "data": {"temperature": 25.3, "humidity": 80.1},
    "timestamp": "2025-01-15T12:00:00+00:00"
}

For status changes:

{
    "event": "status",
    "device_type": "weatherstation",
    "device_id": "ws-001",
    "data": {"old_status": "offline", "new_status": "online"},
    "timestamp": "2025-01-15T12:00:05+00:00"
}

SSE Backend

The SSE backend requires no additional dependencies. It works with any WSGI server that supports streaming responses (gunicorn with sync workers, Django's dev server, etc.).

Configuration

DJANGO_DEVICEHUB = {
    "REALTIME": {
        "BACKEND": "sse",
        "SSE_KEEPALIVE_INTERVAL": 30,   # seconds between keepalive comments
    },
}

URL setup

Include the SSE URLs in your project:

# urls.py
urlpatterns = [
    path("iot/", include("django_devicehub.urls")),
]

This provides four endpoints:

URL Description
/iot/stream/{device_type}/ SSE stream -- all devices of a type
/iot/stream/{device_type}/{device_id}/ SSE stream -- specific device
/iot/poll/{device_type}/ Polling fallback -- all devices of a type
/iot/poll/{device_type}/{device_id}/ Polling fallback -- specific device

All endpoints require authentication (@login_required).

SSE wire format

Events are sent in standard SSE format:

event: connected
data: {"group": "iot_weatherstation"}

event: data
data: {"device_id": "ws-001", "data": {"temperature": 25.3}, ...}

: keepalive

event: status
data: {"device_id": "ws-001", "data": {"old_status": "offline", ...}, ...}

The keepalive comment (: keepalive) is sent every SSE_KEEPALIVE_INTERVAL seconds to prevent proxies and browsers from closing the connection due to inactivity.

Polling fallback

The /iot/poll/{device_type}/ endpoint returns a JSON array of any events that occurred since the last poll. This is a simple fallback for clients that cannot use SSE or WebSockets:

{"events": [
    {"event": "data", "device_type": "weatherstation", "device_id": "ws-001", ...}
]}

Nginx configuration for SSE

Disable response buffering so events are delivered immediately:

location /iot/stream/ {
    proxy_pass http://django-app:8000;
    proxy_set_header Connection '';
    proxy_http_version 1.1;
    proxy_buffering off;
    proxy_cache off;
    chunked_transfer_encoding off;
}

The SSE view already sets X-Accel-Buffering: no and Cache-Control: no-cache headers.

Frontend Integration

Connecting via SSE (JavaScript)

const source = new EventSource("/iot/stream/weatherstation/");

source.addEventListener("connected", (e) => {
    console.log("Connected:", JSON.parse(e.data));
});

source.addEventListener("data", (e) => {
    const msg = JSON.parse(e.data);
    console.log(`Device ${msg.device_id}:`, msg.data);
    // Update dashboard UI
    updateChart(msg.device_id, msg.data.temperature);
});

source.addEventListener("status", (e) => {
    const msg = JSON.parse(e.data);
    console.log(`${msg.device_id} status: ${msg.data.new_status}`);
    updateDeviceStatus(msg.device_id, msg.data.new_status);
});

source.onerror = () => {
    console.warn("SSE connection lost, reconnecting...");
};

Connecting via WebSocket (JavaScript)

const ws = new WebSocket("ws://localhost:8000/ws/iot/weatherstation/");

ws.onopen = () => {
    console.log("WebSocket connected");
};

ws.onmessage = (e) => {
    const msg = JSON.parse(e.data);
    if (msg.event === "data") {
        updateChart(msg.device_id, msg.data);
    } else if (msg.event === "status") {
        updateDeviceStatus(msg.device_id, msg.data.new_status);
    }
};

// Ping to keep connection alive
setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify({action: "ping"}));
    }
}, 30000);

Subscribing to a specific device

// SSE -- specific device
const source = new EventSource("/iot/stream/weatherstation/STATION-001/");

// WebSocket -- specific device
const ws = new WebSocket("ws://localhost:8000/ws/iot/weatherstation/STATION-001/");

Manual Broadcasting

You can broadcast events programmatically without going through MQTT:

from django_devicehub.realtime.bridge import realtime_bridge

realtime_bridge.broadcast(
    device_type="weatherstation",
    device_id="ws-001",
    event_type="data",
    data={"temperature": 25.3, "humidity": 80.1},
)

This pushes the event to all connected WebSocket/SSE clients subscribed to the weatherstation type or the specific ws-001 device.