From feefd55e200c80073e199d1aa41a1f0a53ffb43d Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 17 Dec 2023 21:04:39 +0100 Subject: [PATCH 1/2] Improved code structure --- src/{ => handler}/climate.py | 0 src/{ => handler}/history.py | 0 src/{ => handler}/matrix.py | 0 src/main.py | 51 +++++++++++++++++------------------- 4 files changed, 24 insertions(+), 27 deletions(-) rename src/{ => handler}/climate.py (100%) rename src/{ => handler}/history.py (100%) rename src/{ => handler}/matrix.py (100%) diff --git a/src/climate.py b/src/handler/climate.py similarity index 100% rename from src/climate.py rename to src/handler/climate.py diff --git a/src/history.py b/src/handler/history.py similarity index 100% rename from src/history.py rename to src/handler/history.py diff --git a/src/matrix.py b/src/handler/matrix.py similarity index 100% rename from src/matrix.py rename to src/handler/matrix.py diff --git a/src/main.py b/src/main.py index b8195dd..7c25bc1 100644 --- a/src/main.py +++ b/src/main.py @@ -2,11 +2,11 @@ import os from fastapi import FastAPI, HTTPException from datetime import datetime import requests -from history import get_recent_entries -from matrix import MatrixDisplay +from handler.history import get_recent_entries +from handler.matrix import MatrixDisplay from fastapi.middleware.cors import CORSMiddleware import asyncio -from climate import Dht22Sensor +from handler.climate import Dht22Sensor app = FastAPI() @@ -14,7 +14,7 @@ origins = [ "http://localhost", "http://localhost:8000", "http://raspberrypi", - "http://192.168.178.84" + "http://192.168.178.84", ] app.add_middleware( @@ -32,24 +32,28 @@ climate_log_file = "./climate.csv" matrix_display = MatrixDisplay() dht22_sensor = Dht22Sensor(dht22_pin) + # Start background service to log temperature and humidity every minute async def log_temperature(): # If file does not exist, create it and write header if not os.path.isfile(climate_log_file): with open(climate_log_file, "w") as f: f.write("timestamp,temperature,humidity\n") - + while True: measurements = dht22_sensor.read() if measurements is not None: with open(climate_log_file, "a") as f: - f.write("{},{},{}\n".format( - datetime.now().isoformat(), - measurements["temperature"], - measurements["humidity"] - )) + f.write( + "{},{},{}\n".format( + datetime.now().isoformat(), + measurements["temperature"], + measurements["humidity"], + ) + ) await asyncio.sleep(60) + async def display_time(): while should_run_time_loop: try: @@ -64,12 +68,10 @@ async def display_time(): await asyncio.sleep(seconds_until_next_minute) - asyncio.create_task(display_time()) asyncio.create_task(log_temperature()) - @app.post("/time") async def start_time_loop(): global should_run_time_loop @@ -86,25 +88,22 @@ async def turn_off(): return {"message": "Display turned off"} - @app.post("/temperature") async def temperature(): measurements = dht22_sensor.get_last_read() if measurements is None: return {"message": "Failed to read temperature"} - - + global should_run_time_loop was_clock_runnign = should_run_time_loop should_run_time_loop = False - - + matrix_display.show_text("{0:0.1f}*C".format(measurements["temperature"])) - + if was_clock_runnign: should_run_time_loop = True asyncio.create_task(display_time()) - + return measurements @@ -113,19 +112,17 @@ async def humidity(): measurements = dht22_sensor.get_last_read() if measurements is None: return {"message": "Failed to read humidity"} - - + global should_run_time_loop was_clock_runnign = should_run_time_loop should_run_time_loop = False - - + matrix_display.show_text("{0:0.1f}%".format(measurements["humidity"])) - + if was_clock_runnign: should_run_time_loop = True asyncio.create_task(display_time()) - + return measurements @@ -140,9 +137,9 @@ async def flash(count: int = 1): global should_run_time_loop was_clock_runnign = should_run_time_loop should_run_time_loop = False - + matrix_display.flash(count) - + if was_clock_runnign: should_run_time_loop = True asyncio.create_task(display_time()) From f860108b52134acc4b8accedc033803838b57928 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 18 Dec 2023 00:21:37 +0100 Subject: [PATCH 2/2] Improved code structure and implemented improved async action handling with queue and idle actions --- src/actions.py | 40 ++++++++++++ src/config.py | 2 + src/handler/action_queue.py | 52 +++++++++++++++ src/main.py | 125 ++++++------------------------------ 4 files changed, 114 insertions(+), 105 deletions(-) create mode 100644 src/actions.py create mode 100644 src/config.py create mode 100644 src/handler/action_queue.py diff --git a/src/actions.py b/src/actions.py new file mode 100644 index 0000000..b5354ec --- /dev/null +++ b/src/actions.py @@ -0,0 +1,40 @@ +import asyncio +from datetime import datetime +import os +from config import climate_log_file, dht22_pin +from handler.climate import Dht22Sensor +from handler.matrix import MatrixDisplay + +matrix_display = MatrixDisplay() +dht22_sensor = Dht22Sensor(dht22_pin) + + +async def log_temperature(): + # If file does not exist, create it and write header + if not os.path.isfile(climate_log_file): + with open(climate_log_file, "w") as f: + f.write("timestamp,temperature,humidity\n") + + while True: + measurements = dht22_sensor.read() + if measurements is not None: + with open(climate_log_file, "a") as f: + f.write( + "{},{},{}\n".format( + datetime.now().isoformat(), + measurements["temperature"], + measurements["humidity"], + ) + ) + await asyncio.sleep(60) + + +async def display_time(): + while True: + try: + matrix_display.show_current_time() + except Exception as e: + raise "Failed to display time on the matrix display: {e}" + + seconds_until_next_minute = 60 - datetime.now().second + await asyncio.sleep(seconds_until_next_minute) diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..b52c857 --- /dev/null +++ b/src/config.py @@ -0,0 +1,2 @@ +dht22_pin = 17 +climate_log_file = "./climate.csv" diff --git a/src/handler/action_queue.py b/src/handler/action_queue.py new file mode 100644 index 0000000..5413d47 --- /dev/null +++ b/src/handler/action_queue.py @@ -0,0 +1,52 @@ +import asyncio +from typing import Optional +from enum import Enum + + +class QueueState(Enum): + STOPPED = 0 + IDLE = 1 + POPPING = 2 # As in: Popping actions from the queue and performing them + + +class ActionQueue: + def __init__(self, idle_action=None) -> None: + self.queued_actions: list = [] + self.idle_action: Optional[any] = None + self.state = QueueState.STOPPED + self.background_task = None + + if idle_action: + self.set_idle_action(idle_action) + + async def run_queue(self): + while len(self.queued_actions) > 0: + self.state = QueueState.POPPING + action = self.queued_actions.pop(0) + action[0](*(action[1]), **(action[2])) + + self.state = QueueState.IDLE + self.idle_action() + + async def stop_queue(self): + if self.background_task is None: + return + + self.state = QueueState.STOPPED + self.background_task.cancel() + + async def restart_queue(self): + await self.stop_queue() + self.background_task = asyncio.create_task(self.run_queue()) + + async def add_action_to_queue(self, action, *args, **kwargs): + self.queued_actions.append((action, args, kwargs)) + + if self.state == QueueState.IDLE: + await self.restart_queue() + + async def set_idle_action(self, action): + self.idle_action = action + + if self.state == QueueState.IDLE: + await self.restart_queue() diff --git a/src/main.py b/src/main.py index 7c25bc1..79a022a 100644 --- a/src/main.py +++ b/src/main.py @@ -1,13 +1,15 @@ -import os -from fastapi import FastAPI, HTTPException -from datetime import datetime -import requests -from handler.history import get_recent_entries -from handler.matrix import MatrixDisplay -from fastapi.middleware.cors import CORSMiddleware import asyncio -from handler.climate import Dht22Sensor +import requests +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware + +from actions import dht22_sensor, display_time, log_temperature, matrix_display +from config import climate_log_file +from handler.action_queue import ActionQueue +from handler.history import get_recent_entries + +queue = ActionQueue(display_time) app = FastAPI() origins = [ @@ -25,66 +27,18 @@ app.add_middleware( allow_headers=["*"], ) -should_run_time_loop = True -dht22_pin = 17 -climate_log_file = "./climate.csv" - -matrix_display = MatrixDisplay() -dht22_sensor = Dht22Sensor(dht22_pin) - - -# Start background service to log temperature and humidity every minute -async def log_temperature(): - # If file does not exist, create it and write header - if not os.path.isfile(climate_log_file): - with open(climate_log_file, "w") as f: - f.write("timestamp,temperature,humidity\n") - - while True: - measurements = dht22_sensor.read() - if measurements is not None: - with open(climate_log_file, "a") as f: - f.write( - "{},{},{}\n".format( - datetime.now().isoformat(), - measurements["temperature"], - measurements["humidity"], - ) - ) - await asyncio.sleep(60) - - -async def display_time(): - while should_run_time_loop: - try: - matrix_display.show_current_time() - except requests.exceptions.RequestException as e: - raise HTTPException( - status_code=500, - detail=f"Failed to display time on the matrix display: {e}", - ) - - seconds_until_next_minute = 60 - datetime.now().second - await asyncio.sleep(seconds_until_next_minute) - - -asyncio.create_task(display_time()) asyncio.create_task(log_temperature()) @app.post("/time") async def start_time_loop(): - global should_run_time_loop - should_run_time_loop = True - asyncio.create_task(display_time()) + await queue.set_idle_action(display_time) return {"message": "Time loop started"} @app.post("/off") async def turn_off(): - global should_run_time_loop - should_run_time_loop = False - matrix_display.turn_off() + await queue.set_idle_action(matrix_display.turn_off) return {"message": "Display turned off"} @@ -94,15 +48,9 @@ async def temperature(): if measurements is None: return {"message": "Failed to read temperature"} - global should_run_time_loop - was_clock_runnign = should_run_time_loop - should_run_time_loop = False - - matrix_display.show_text("{0:0.1f}*C".format(measurements["temperature"])) - - if was_clock_runnign: - should_run_time_loop = True - asyncio.create_task(display_time()) + await queue.add_action_to_queue( + matrix_display.show_text, "{0:0.1f}*C".format(measurements["temperature"]) + ) return measurements @@ -113,15 +61,9 @@ async def humidity(): if measurements is None: return {"message": "Failed to read humidity"} - global should_run_time_loop - was_clock_runnign = should_run_time_loop - should_run_time_loop = False - - matrix_display.show_text("{0:0.1f}%".format(measurements["humidity"])) - - if was_clock_runnign: - should_run_time_loop = True - asyncio.create_task(display_time()) + await queue.add_action_to_queue( + matrix_display.show_text, "{0:0.1f}%".format(measurements["humidity"]) + ) return measurements @@ -134,40 +76,13 @@ async def history(): @app.post("/flash") async def flash(count: int = 1): - global should_run_time_loop - was_clock_runnign = should_run_time_loop - should_run_time_loop = False + await queue.add_action_to_queue(matrix_display.flash, count) - matrix_display.flash(count) - - if was_clock_runnign: - should_run_time_loop = True - asyncio.create_task(display_time()) return {"message": "Display flashed"} @app.post("/message") async def display_message(body: dict): - global should_run_time_loop - was_clock_runnign = should_run_time_loop - should_run_time_loop = False message_text = body.get("message") - try: - matrix_display.show_text(message_text) - except requests.exceptions.RequestException as e: - raise HTTPException( - status_code=500, - detail=f"Failed to display message on the matrix display: {e}", - ) - finally: - if was_clock_runnign: - should_run_time_loop = True - asyncio.create_task(display_time()) + await queue.add_action_to_queue(matrix_display.show_text, message_text) return {"message": "Message displayed"} - - -@app.post("/stop") -async def stop_time_loop(): - global should_run_time_loop - should_run_time_loop = False - return {"message": "Time loop stopped"}