Search code examples
pythonmultithreadingwebsockettornado

Make client.write_message in python thread


I am currently making a websocket server with python Tornado to manage the home automation of my house. The principle is as follows: from a web page users send a command (example: turn on the watering), the server receives the command and activates a relay connected to an esp32.

To create watering schedules I thought I could create a thread with an infinite loop that looks at what time is saved in the database, do a time.sleep of the watering time minus the current time then start the watering. For this part it's good everything works fine. But after that I have to communicate to the connected user that the watering is on and therefore do a client.write_message('the watering is on') but unfortunately tornado does not allow it. So how can I go about achieving something like this?

On some forums people propose to make a queue with the commands which are processed one by one further with an infinite while loop but that does not suit me it blocks my program.

Thank you in advance for your answers.

class WSHandler(tornado.websocket.WebSocketHandler):
    def wateringProgram():
            time.sleep(secondsBeforeOnWatering) # secondsBeforeOnWatering in a db
            watering.on()
            for client in clients:
                client.write_message('watering is on')
            time.sleep(wateringDuration) # wateringDuration in a db
            watering.off()
            for client in clients:
                client.write_message('watering is off')
    
    threadProgram = threading.Thread(target=wateringProgram, args=())
    threadProgram.start()
    
    def open(self):
        WSHandler.clients.add(self) 
        self.write_message('logs')
        print ('[SERVEUR] Connection was opened')

    def on_message(self, message):
        if message == 'program changing':
            threadProgram.restart() # restart with the new timing

    def on_close(self):
        WSHandler.clients.remove(self)
        print ('[WS] Connection was closed.')

application = tornado.web.Application([
    (r'/ws', WSHandler),
], **settings)

if __name__ == "__main__":
    try:
        http_server = tornado.httpserver.HTTPServer(application)
        http_server.listen(PORT)
        main_loop = tornado.ioloop.IOLoop.instance()

        print ("Tornado Server started")
        main_loop.start()
    except:
        print ("Exception triggered - Tornado Server stopped.")

The above code is simplified to be more concise


Solution

  • I don't think you can send messages from a thread in Tornado. You'll have to call the main thread to pass the messages.

    First, pass the current IOLoop instance to the wateringProgram function.
    Seciond, use IOLoop.add_callback to send messages from the main thread.

    Here's an example:

    class WSHandler(tornado.websocket.WebSocketHandler):
        def wateringProgram(loop):
            time.sleep(secondsBeforeOnWatering) # secondsBeforeOnWatering in a db
            watering.on()
    
            loop.add_callback(WSHandler.send_message_to_all, msg='watering is on')
    
            time.sleep(wateringDuration) # wateringDuration in a db
            watering.off()
    
            loop.add_callback(WSHandler.send_message_to_all, msg='watering is on')
    
        
        threadProgram = threading.Thread(target=wateringProgram, args=(tornado.ioloop.IOLoop.current(),))
        threadProgram.start()
    
    
        @classmethod
        def send_message_to_all(cls, msg):
            for client in cls.clients:
                client.write_message(msg)