Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ pytesseract
requests
PyPDF2
chromadb
psutil
psutil
apscheduler
httpx
20 changes: 19 additions & 1 deletion backend/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# lan_server/server.py
import os
import secrets
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi import Header, HTTPException
from fastapi.middleware.cors import CORSMiddleware
Expand Down Expand Up @@ -69,8 +70,25 @@ def verify_admin(x_admin_key: str = Header(None)):
print(f"🚫 Admin Access Denied: Key mismatch. Received length: {len(clean_key)} (Expected: {len(clean_secret)}), Masked input: {masked_key}")
print("💡 TIP: Copy the EXACT random secret from the server logs above.")
raise HTTPException(status_code=403, detail="Admin access denied")

# ================= BACKGROUND TASKS & SCHEDULER =================
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from tasks import check_morning_weather

@asynccontextmanager
async def lifespan(app: FastAPI):
# Setup Scheduler
scheduler = AsyncIOScheduler(timezone='Asia/Kolkata')
# Run daily at 07:00 AM
scheduler.add_job(check_morning_weather, 'cron', hour=7, minute=0)
scheduler.start()
print("🌅 Weather Sentinel scheduler started (Runs at 07:00 AM IST).")
yield
scheduler.shutdown()
print("🌅 Weather Sentinel scheduler stopped.")

# ================= FASTAPI APP =================
app = FastAPI(title="LAN Chat Server (modular)")
app = FastAPI(title="LAN Chat Server (modular)", lifespan=lifespan)

# CORS: allow origins (disabled credentials for security)
app.add_middleware(
Expand Down
106 changes: 106 additions & 0 deletions backend/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import asyncio
import json
import logging
from datetime import datetime, timedelta, timezone

import httpx

from chat import connected_clients, send_to_user
from lumir.ai_engine import ask_ai
from messages import create_delivery_entries, save_message
from users import get_ai_config, get_all_users, get_default_location

LOGGER = logging.getLogger(__name__)

IST = timezone(timedelta(hours=5, minutes=30))
DEFAULT_LAT = 26.2183
DEFAULT_LON = 78.1828


def _get_default_coordinates() -> tuple[float, float]:
try:
return get_default_location()
except Exception:
LOGGER.debug("Weather sentinel could not read configured coordinates.", exc_info=True)
return DEFAULT_LAT, DEFAULT_LON


async def _broadcast_family_warning(message_text: str) -> None:
timestamp = int(datetime.now(IST).timestamp() * 1000)
payload = {
"type": "text",
"text": message_text,
"sender": "Lumir",
"receiver": "Family Group",
"timestamp": timestamp,
}

msg_id = save_message(
text=message_text,
sender="Lumir",
receiver="Family Group",
msg_type="text",
)

recipients = [u["username"] for u in get_all_users() if u.get("username") != "Lumir"]
if recipients:
create_delivery_entries(msg_id, recipients)

payload_json = json.dumps(payload)
for username in list(connected_clients.keys()):
await send_to_user(username, payload_json)


async def check_morning_weather() -> None:
try:
lat, lon = _get_default_coordinates()
location = f"{lat:.4f}, {lon:.4f}"

async with httpx.AsyncClient(timeout=15.0) as client:
response = await client.get(
"https://api.open-meteo.com/v1/forecast",
params={
"latitude": lat,
"longitude": lon,
"daily": "precipitation_sum,temperature_2m_max",
"timezone": "Asia/Kolkata",
"forecast_days": 1,
},
)
response.raise_for_status()
forecast = response.json().get("daily", {})

precipitation_values = forecast.get("precipitation_sum") or []
max_temp_values = forecast.get("temperature_2m_max") or []

precipitation_sum = float(precipitation_values[0]) if precipitation_values else 0.0
temperature_2m_max = float(max_temp_values[0]) if max_temp_values else 0.0

alert_reason = None
if precipitation_sum > 2.0:
alert_reason = f"{precipitation_sum:.1f}mm rain"
elif temperature_2m_max > 40.0:
alert_reason = f"{temperature_2m_max:.1f}°C heat"

if not alert_reason:
return

hidden_prompt = (
"System Context: Aaj {location} mein {alert_reason} hone wali hai. "
"Act like a caring family member and write a warning message for the Family Group. "
"Keep it under 15 words. Do NOT use markdown. Say good morning."
).format(location=location, alert_reason=alert_reason)

ai_config = get_ai_config()
generated_message = await asyncio.to_thread(
ask_ai,
prompt=hidden_prompt,
config=ai_config,
history=[],
sender="System",
)

await _broadcast_family_warning((generated_message or "Good morning. Please stay safe today.").strip())

except Exception:
LOGGER.debug("Weather sentinel failed during scheduled execution.", exc_info=True)
43 changes: 43 additions & 0 deletions backend/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,49 @@ def init_db():


# --- Config Helpers ---


# --- Weather Location Config Helpers ---
def get_default_location():
"""
Fetch weather default location from config table.
Falls back to Maheshpura coordinates when missing/invalid.
"""
lat = 26.2183
lon = 78.1828

conn = sqlite3.connect(DATABASE_NAME)
cursor = conn.cursor()
cursor.execute("SELECT key, value FROM config WHERE key IN ('default_lat', 'default_lon')")
rows = cursor.fetchall()
conn.close()

for key, value in rows:
try:
if key == "default_lat":
lat = float(value)
elif key == "default_lon":
lon = float(value)
except (TypeError, ValueError):
continue

return lat, lon


def set_default_location(lat: float, lon: float):
conn = sqlite3.connect(DATABASE_NAME)
cursor = conn.cursor()
cursor.execute(
"INSERT OR REPLACE INTO config (key, value) VALUES ('default_lat', ?)",
(str(lat),),
)
cursor.execute(
"INSERT OR REPLACE INTO config (key, value) VALUES ('default_lon', ?)",
(str(lon),),
)
conn.commit()
conn.close()

def set_require_approval(enabled: bool):
conn = sqlite3.connect(DATABASE_NAME)
cursor = conn.cursor()
Expand Down