Websocket Connection to Django Channels

Want to build Django Channels Webserver (SQLite) with real time updates from multiple raspberry pi’s (Message on signal change) over a websocket connection.

Therefore i tried to build a simple python script running on the raspberry pi, opening a websocket connection to the Webserver and sending a value update over websocket immediately when a value changes, and only the value changed.

Each value update ought to be stored into the Django database.

A client should be able to access via simple browser application (JavaScript/HTML/CSS) the database and filter it for criteria (like location, battery state, etc.). The visualised data ought to be in real-time, therefore maybe keeping updated directly via channels from the raspberry pi updates.

I ruled out MQTT, since there seems not to be a propper integration into Django.
I wanted to avoid REST API, since it does not provide real real-time behaviour (allthough i could of course send updates every some seconds).

My Proof-of-Concept attempts have been conducted locally on one machine, but i failed already in connecting to the django channels server via python websockets script.

django project name: solserver
django appname: database

websocket-client:

import asyncio
import websockets

class Base():
    """Basic class, containing common functionality among all versions for Raspberry Pi."""
    def __init__(self, serial_number: str, software: str, country: str, postcode: str) -> None:
        self.serial_number = serial_number
        self.software = software # software version
        self.country = country # country
        self.postcode = postcode # postcode
        self.battery = 0 # % - state of charge of the battery

        self.server_url = 'ws://localhost:8000/update/'
        self.publish = True
   
    async def publish_state(self, interval: int = 2):
        """Coro: Periodically publishes instance state to websocket URL."""
        async with websockets.connect(self.server_url) as ws:
            print(f'connected to {self.server_url}')
            while self.publish:
                await ws.send('hello')
                # response = await ws.recv()
                # print(f'response: {response}')
                await asyncio.sleep(interval)

async def main():
    rpi = Base("1234","1.0","at","1040")
    await rpi.publish_state()
    
if __name__ == "__main__":
    print(f'running {__file__}')
    asyncio.run(main())

consumers.py

from channels.generic.websocket import AsyncWebsocketConsumer

class ServerConsumer(AsyncWebsocketConsumer):
    groups = ["broadcast"]

    async def connect(self):
        await self.accept()

routing.py

from django.urls import re_path
from . import consumers

websocket_urlpatterns = [
    re_path("update/", consumers.ServerConsumer.as_asgi()),
]

asgi.py

import os

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from django.core.asgi import get_asgi_application
from database.routing import websocket_urlpatterns

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'solserver.settings')
# Initialize Django ASGI application early to ensure the AppRegistry
# is populated before importing code that may import ORM models.
django_asgi_app = get_asgi_application()

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

settings.py

...

INSTALLED_APPS = [
    'daphne',
    'database',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

...

Django Channels Server log:

python .\manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
March 07, 2024 - 16:28:19
Django version 5.0.3, using settings 'solserver.settings'
Starting ASGI/Daphne version 4.1.0 development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
WebSocket HANDSHAKING /update/ [127.0.0.1:55755]
WebSocket REJECT /update/ [127.0.0.1:55755]
WebSocket DISCONNECT /update/ [127.0.0.1:55755]

websocket-client log: (ignore the slightly different file naming on my computer)

running v2.py
Traceback (most recent call last):
  File "v2.py", line 42, in <module>
    asyncio.run(main())
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\asyncio\runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\asyncio\base_events.py", line 649, in run_until_complete
    return future.result()
  File "v2.py", line 41, in main
    await v2.publish_state()
  File "base.py", line 39, in publish_state
    async with websockets.connect(self.server_url) as ws:
  File "client.py", line 629, in __aenter__
    return await self
  File "client.py", line 647, in __await_impl_timeout__
    return await self.__await_impl__()
  File "client.py", line 654, in __await_impl__       
    await protocol.handshake(
  File "client.py", line 325, in handshake
    raise InvalidStatusCode(status_code, response_headers)
websockets.exceptions.InvalidStatusCode: server rejected WebSocket connection: HTTP 403

Utilizing:
Python 3.10.11
Django 5.0.3
Channels 4.0
Docker version 25.0.3
(for Redis Image)
Daphne 4.1.0

Thanks for your thoughts in advance!

2 Likes

Short versions - ask if you need any of these elements explained in more detail:

  • Remove the AllowedHostsOriginValidator. The HTTP request to establish the websocket is not coming from a browser and so does not have all the headers that the browser would supply.

  • Remove the AuthMiddlewareStack reference. These clients are not performing HTTP authentication

  • You’re identifying the groups attribute in your consumer. This means you need to have the channels layer configured. Do you have that in your settings.py file?

There may be other issues here, but these are what jump out at me right now.

1 Like

AllowedHostsOriginValidator and AuthMiddlewareStack caused the issue.
I took it from the official introduction to Django Channels and didn’t put it into question.

Thanks a lot, especially for this amazing fast response!

1 Like

Update: Find the finished project, properly documented, here