Compare commits

...

9 Commits

Author SHA1 Message Date
7e752562bc Remove logging 2025-08-01 21:04:42 +00:00
f1d246aa31 refactor: make controller_message async using aiohttp
Co-authored-by: aider (gemini/gemini-2.5-pro-preview-05-06) <aider@aider.chat>
2025-08-01 21:04:42 +00:00
5b0573abc7 feat: send open door alerts to controller 2025-08-01 21:04:42 +00:00
13ef45f72a fix: only reset alerts when door state is 'closed'
Co-authored-by: aider (gemini/gemini-2.5-pro-preview-05-06) <aider@aider.chat>
2025-08-01 21:04:42 +00:00
bb1da6d836 feat: add timed alerts for open garage door
Co-authored-by: aider (gemini/gemini-2.5-pro-preview-05-06) <aider@aider.chat>
2025-08-01 21:04:37 +00:00
f0f16a6841 fix: remove array from door state response 2025-08-01 21:02:35 +00:00
7a26f91cf1 feat: Return door state as JSON
Co-authored-by: aider (gemini/gemini-2.5-pro-preview-05-06) <aider@aider.chat>
2025-08-01 21:02:35 +00:00
2556858912 refactor: move get_derived_state function to top of file 2025-08-01 21:02:35 +00:00
20c433af2d feat: save high-confidence images to sorted directories
Co-authored-by: aider (gemini/gemini-2.5-pro-preview-05-06) <aider@aider.chat>
2025-08-01 21:02:35 +00:00

102
server.py
View File

@@ -9,7 +9,7 @@ import asyncio
import aiohttp import aiohttp
from aiohttp import web from aiohttp import web
import io import io
from datetime import datetime from datetime import datetime, timedelta
import torch import torch
import torch.nn.functional as F import torch.nn.functional as F
@@ -32,6 +32,36 @@ UNSURE_CONFIDENCE_THRESHOLD = 0.97
PREDICTION_HISTORY = [] PREDICTION_HISTORY = []
PREDICTION_HISTORY_MAX_LENGTH = 3 PREDICTION_HISTORY_MAX_LENGTH = 3
PREVIOUS_STATE = "unknown" PREVIOUS_STATE = "unknown"
LAST_OPEN_SAVE_TIME = None
DOOR_OPEN_START_TIME = None
OPEN_ALERT_THRESHOLDS_MINUTES = [5, 15, 30, 60, 120]
OPEN_ALERTS_SENT_FOR_CURRENT_OPENING = []
async def controller_message(app, message):
payload = {mysecrets.CONTROLLER_KEY: message}
session = app['client_session']
try:
async with session.post(mysecrets.CONTROLLER_URL, data=payload, timeout=10) as response:
if response.status == 200:
return True
else:
logging.error(f'Unable to communicate with controller! Message: {message}, Status: {response.status}')
return False
except Exception:
logging.exception('Unable to communicate with controller! Message: ' + message)
return False
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
# --- Model Inference --- # --- Model Inference ---
def get_prediction(model, image_bytes, device): def get_prediction(model, image_bytes, device):
@@ -61,6 +91,7 @@ def get_prediction(model, image_bytes, device):
# --- Background Task --- # --- Background Task ---
async def monitor_garage_door(app): async def monitor_garage_door(app):
"""Periodically fetches an image and logs the garage door status.""" """Periodically fetches an image and logs the garage door status."""
global LAST_OPEN_SAVE_TIME
logging.info("Starting garage door monitoring task.") logging.info("Starting garage door monitoring task.")
session = app['client_session'] session = app['client_session']
model = app['model'] model = app['model']
@@ -89,11 +120,10 @@ async def monitor_garage_door(app):
if len(PREDICTION_HISTORY) > PREDICTION_HISTORY_MAX_LENGTH: if len(PREDICTION_HISTORY) > PREDICTION_HISTORY_MAX_LENGTH:
PREDICTION_HISTORY.pop(0) PREDICTION_HISTORY.pop(0)
timestamp = datetime.now().isoformat().replace(':', '-')
filename = f"{timestamp}.jpg"
if confidence < UNSURE_CONFIDENCE_THRESHOLD: if confidence < UNSURE_CONFIDENCE_THRESHOLD:
# Sanitize timestamp for use in filename
timestamp = datetime.now().isoformat().replace(':', '-')
filename = f"{timestamp}.jpg"
# Construct path and save file # Construct path and save file
unsure_dir = os.path.join('data', 'unsure', prediction) unsure_dir = os.path.join('data', 'unsure', prediction)
os.makedirs(unsure_dir, exist_ok=True) os.makedirs(unsure_dir, exist_ok=True)
@@ -103,6 +133,29 @@ async def monitor_garage_door(app):
f.write(image_bytes) f.write(image_bytes)
logging.info(f"Low confidence prediction: {prediction} ({confidence:.4f}). Saved for review: {filepath}") logging.info(f"Low confidence prediction: {prediction} ({confidence:.4f}). Saved for review: {filepath}")
else:
# High confidence, save to sorted
if get_derived_state() == 'open':
if LAST_OPEN_SAVE_TIME is None or (datetime.now() - LAST_OPEN_SAVE_TIME) > timedelta(minutes=5):
sorted_dir = os.path.join('data', 'sorted', 'open')
os.makedirs(sorted_dir, exist_ok=True)
filepath = os.path.join(sorted_dir, filename)
with open(filepath, 'wb') as f:
f.write(image_bytes)
LAST_OPEN_SAVE_TIME = datetime.now()
logging.info(f"Saved high-confidence 'open' image: {filepath}")
elif get_derived_state() == 'closed':
open_dir = os.path.join('data', 'sorted', 'open')
closed_dir = os.path.join('data', 'sorted', 'closed')
os.makedirs(open_dir, exist_ok=True)
os.makedirs(closed_dir, exist_ok=True)
num_open = len(os.listdir(open_dir))
num_closed = len(os.listdir(closed_dir))
if num_closed < num_open:
filepath = os.path.join(closed_dir, filename)
with open(filepath, 'wb') as f:
f.write(image_bytes)
logging.info(f"Saved high-confidence 'closed' image: {filepath}")
else: else:
logging.error(f"Failed to fetch image. Status: {response.status}, Reason: {response.reason}") logging.error(f"Failed to fetch image. Status: {response.status}, Reason: {response.reason}")
@@ -129,16 +182,34 @@ async def monitor_garage_door(app):
async def monitor_state_transitions(app): async def monitor_state_transitions(app):
"""Periodically checks for state transitions and logs them.""" """Periodically checks for state transitions and logs them."""
global PREVIOUS_STATE global PREVIOUS_STATE, DOOR_OPEN_START_TIME, OPEN_ALERTS_SENT_FOR_CURRENT_OPENING
logging.info("Starting state transition monitoring task.") logging.info("Starting state transition monitoring task.")
while True: while True:
try: try:
await asyncio.sleep(5) await asyncio.sleep(5)
current_state = get_derived_state() current_state = get_derived_state()
if current_state != "unknown": if current_state != "unknown" and current_state != PREVIOUS_STATE:
if current_state != PREVIOUS_STATE: logging.info(f"State transitioned from '{PREVIOUS_STATE}' to '{current_state}'.")
logging.info(f"State transitioned from '{PREVIOUS_STATE}' to '{current_state}'.")
PREVIOUS_STATE = current_state PREVIOUS_STATE = current_state
if current_state == 'open':
if DOOR_OPEN_START_TIME is None:
DOOR_OPEN_START_TIME = datetime.now()
OPEN_ALERTS_SENT_FOR_CURRENT_OPENING = []
open_duration = datetime.now() - DOOR_OPEN_START_TIME
open_duration_minutes = open_duration.total_seconds() / 60
for threshold in OPEN_ALERT_THRESHOLDS_MINUTES:
if open_duration_minutes >= threshold and threshold not in OPEN_ALERTS_SENT_FOR_CURRENT_OPENING:
msg = f"ALERT: Garage door has been open for {threshold} minutes."
await controller_message(app, msg)
logging.info(msg)
OPEN_ALERTS_SENT_FOR_CURRENT_OPENING.append(threshold)
elif current_state == 'closed':
DOOR_OPEN_START_TIME = None
OPEN_ALERTS_SENT_FOR_CURRENT_OPENING = []
except asyncio.CancelledError: except asyncio.CancelledError:
logging.info("State transition monitoring task cancelled.") logging.info("State transition monitoring task cancelled.")
break break
@@ -148,17 +219,6 @@ async def monitor_state_transitions(app):
# --- 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")
@@ -166,7 +226,7 @@ async def handle_root(request):
async def handle_state(request): async def handle_state(request):
"""Handler for the /state GET request.""" """Handler for the /state GET request."""
state = get_derived_state() state = get_derived_state()
return web.Response(text=state) return web.json_response({'door': state})
async def on_startup(app): async def on_startup(app):
"""Actions to perform on application startup.""" """Actions to perform on application startup."""