Compare commits
9 Commits
eb5f9ba00a
...
7e752562bc
Author | SHA1 | Date | |
---|---|---|---|
7e752562bc | |||
f1d246aa31 | |||
5b0573abc7 | |||
13ef45f72a | |||
bb1da6d836 | |||
f0f16a6841 | |||
7a26f91cf1 | |||
2556858912 | |||
20c433af2d |
102
server.py
102
server.py
@@ -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."""
|
||||||
|
Reference in New Issue
Block a user