Compare commits

...

4 Commits

Author SHA1 Message Date
16709de883 Add example secrets file 2025-07-31 19:39:56 -06:00
59b02e18e9 Freeze requirements 2025-07-31 19:39:10 -06:00
de4c99bc1d feat: add background task to log state transitions
Co-authored-by: aider (gemini/gemini-2.5-pro-preview-05-06) <aider@aider.chat>
2025-07-31 19:37:21 -06:00
914e8f9ce8 refactor: Move secrets to module and improve logging config 2025-07-31 19:37:08 -06:00
4 changed files with 82 additions and 15 deletions

1
.gitignore vendored
View File

@@ -143,6 +143,7 @@ sdkconfig.old
data/ data/
secrets.py secrets.py
mysecrets.py
secrets.h secrets.h
*.bin *.bin
output.* output.*

0
mysecrets.py.example Normal file
View File

37
requirements.txt Normal file
View File

@@ -0,0 +1,37 @@
aiohappyeyeballs==2.6.1
aiohttp==3.12.15
aiosignal==1.4.0
attrs==25.3.0
filelock==3.18.0
frozenlist==1.7.0
fsspec==2025.7.0
idna==3.10
jinja2==3.1.6
markupsafe==3.0.2
mpmath==1.3.0
multidict==6.6.3
networkx==3.5
numpy==2.3.2
nvidia-cublas-cu12==12.6.4.1
nvidia-cuda-cupti-cu12==12.6.80
nvidia-cuda-nvrtc-cu12==12.6.77
nvidia-cuda-runtime-cu12==12.6.77
nvidia-cudnn-cu12==9.5.1.17
nvidia-cufft-cu12==11.3.0.4
nvidia-cufile-cu12==1.11.1.6
nvidia-curand-cu12==10.3.7.77
nvidia-cusolver-cu12==11.7.1.2
nvidia-cusparse-cu12==12.5.4.2
nvidia-cusparselt-cu12==0.6.3
nvidia-nccl-cu12==2.26.2
nvidia-nvjitlink-cu12==12.6.85
nvidia-nvtx-cu12==12.6.77
pillow==11.3.0
propcache==0.3.2
setuptools==80.9.0
sympy==1.14.0
torch==2.7.1
torchvision==0.22.1
triton==3.3.1
typing-extensions==4.14.1
yarl==1.20.1

View File

@@ -1,8 +1,13 @@
import os, logging
DEBUG = os.environ.get('DEBUG')
logging.basicConfig(
format='[%(asctime)s] %(levelname)s %(module)s/%(funcName)s - %(message)s',
level=logging.DEBUG if DEBUG else logging.INFO)
logging.getLogger('aiohttp').setLevel(logging.DEBUG if DEBUG else logging.WARNING)
import asyncio import asyncio
import aiohttp import aiohttp
from aiohttp import web from aiohttp import web
import logging
import os
import io import io
from datetime import datetime from datetime import datetime
@@ -11,16 +16,13 @@ import torch.nn.functional as F
from torchvision import transforms from torchvision import transforms
from PIL import Image from PIL import Image
import mysecrets
from model import (CropLowerRightTriangle, GarageDoorCNN, TRIANGLE_CROP_WIDTH, from model import (CropLowerRightTriangle, GarageDoorCNN, TRIANGLE_CROP_WIDTH,
TRIANGLE_CROP_HEIGHT, RESIZE_DIM) TRIANGLE_CROP_HEIGHT, RESIZE_DIM)
# --- Configuration --- # --- Configuration ---
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
BLUEIRIS_KEY = os.getenv('BLUEIRIS_KEY')
if not BLUEIRIS_KEY:
raise ValueError("BLUEIRIS_KEY environment variable not set.")
CAMERA_URL = "http://cameras.dns.t0.vc/image/SE-S?&w=9999&decode=1" CAMERA_URL = "http://cameras.dns.t0.vc/image/SE-S?&w=9999&decode=1"
MODEL_PATH = 'garage_door_cnn.pth' MODEL_PATH = 'garage_door_cnn.pth'
CLASS_NAMES = ['closed', 'open'] # From training, sorted alphabetically CLASS_NAMES = ['closed', 'open'] # From training, sorted alphabetically
@@ -29,6 +31,7 @@ REQUEST_TIMEOUT_SECONDS = 5
UNSURE_CONFIDENCE_THRESHOLD = 0.97 UNSURE_CONFIDENCE_THRESHOLD = 0.97
PREDICTION_HISTORY = [] PREDICTION_HISTORY = []
PREDICTION_HISTORY_MAX_LENGTH = 3 PREDICTION_HISTORY_MAX_LENGTH = 3
PREVIOUS_STATE = "unknown"
# --- Model Inference --- # --- Model Inference ---
def get_prediction(model, image_bytes, device): def get_prediction(model, image_bytes, device):
@@ -62,7 +65,7 @@ async def monitor_garage_door(app):
session = app['client_session'] session = app['client_session']
model = app['model'] model = app['model']
device = app['device'] device = app['device']
headers = {'Authorization': 'Basic ' + BLUEIRIS_KEY} headers = {'Authorization': 'Basic ' + mysecrets.BLUEIRIS_KEY}
while True: while True:
try: try:
@@ -124,21 +127,44 @@ async def monitor_garage_door(app):
await asyncio.sleep(5) await asyncio.sleep(5)
async def monitor_state_transitions(app):
"""Periodically checks for state transitions and logs them."""
global PREVIOUS_STATE
logging.info("Starting state transition monitoring task.")
while True:
try:
await asyncio.sleep(5)
current_state = get_derived_state()
if current_state != "unknown" and current_state != PREVIOUS_STATE:
logging.info(f"State transitioned from '{PREVIOUS_STATE}' to '{current_state}'.")
PREVIOUS_STATE = current_state
except asyncio.CancelledError:
logging.info("State transition monitoring task cancelled.")
break
except Exception as e:
logging.error(f"An unexpected error occurred in the state monitoring task: {e}", exc_info=True)
await asyncio.sleep(5)
# --- Web Server --- # --- Web Server ---
def get_derived_state():
"""Derives the state from the prediction history."""
state = "unknown"
if len(PREDICTION_HISTORY) == PREDICTION_HISTORY_MAX_LENGTH:
if all(s == "open" for s in PREDICTION_HISTORY):
state = "open"
elif all(s == "closed" for s in PREDICTION_HISTORY):
state = "closed"
return state
async def handle_root(request): async def handle_root(request):
"""Handler for the root GET request.""" """Handler for the root GET request."""
return web.Response(text="hello world") return web.Response(text="hello world")
async def handle_state(request): async def handle_state(request):
"""Handler for the /state GET request.""" """Handler for the /state GET request."""
state = "unknown" state = get_derived_state()
if len(PREDICTION_HISTORY) == PREDICTION_HISTORY_MAX_LENGTH:
if all(s == "open" for s in PREDICTION_HISTORY):
state = "open"
elif all(s == "closed" for s in PREDICTION_HISTORY):
state = "closed"
return web.Response(text=state) return web.Response(text=state)
async def on_startup(app): async def on_startup(app):
@@ -160,13 +186,16 @@ async def on_startup(app):
# Start background task # Start background task
app['monitor_task'] = asyncio.create_task(monitor_garage_door(app)) app['monitor_task'] = asyncio.create_task(monitor_garage_door(app))
app['state_monitor_task'] = asyncio.create_task(monitor_state_transitions(app))
async def on_cleanup(app): async def on_cleanup(app):
"""Actions to perform on application cleanup.""" """Actions to perform on application cleanup."""
logging.info("Cleaning up...") logging.info("Cleaning up...")
app['monitor_task'].cancel() app['monitor_task'].cancel()
app['state_monitor_task'].cancel()
try: try:
await app['monitor_task'] await app['monitor_task']
await app['state_monitor_task']
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
await app['client_session'].close() await app['client_session'].close()