Files
Synculous-2/scheduler/daemon.py

177 lines
6.1 KiB
Python

"""
daemon.py - Background polling loop for scheduled tasks
Override poll_callback() with your domain-specific logic.
"""
import os
import time
import logging
from datetime import datetime, timezone, timedelta
import core.postgres as postgres
import core.notifications as notifications
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL", 60))
def _user_now_for(user_uuid):
"""Get current datetime in a user's timezone using their stored offset."""
prefs = postgres.select_one("user_preferences", {"user_uuid": user_uuid})
offset_minutes = 0
if prefs and prefs.get("timezone_offset") is not None:
offset_minutes = prefs["timezone_offset"]
# JS getTimezoneOffset: positive = behind UTC, so negate
tz_obj = timezone(timedelta(minutes=-offset_minutes))
return datetime.now(tz_obj)
def check_medication_reminders():
"""Check for medications due now and send notifications."""
try:
from datetime import date as date_type
meds = postgres.select("medications", where={"active": True})
# Group by user so we only look up timezone once per user
user_meds = {}
for med in meds:
uid = med.get("user_uuid")
if uid not in user_meds:
user_meds[uid] = []
user_meds[uid].append(med)
for user_uuid, user_med_list in user_meds.items():
now = _user_now_for(user_uuid)
current_time = now.strftime("%H:%M")
current_day = now.strftime("%a").lower()
today = now.date()
today_str = today.isoformat()
for med in user_med_list:
freq = med.get("frequency", "daily")
# Skip as_needed -- no scheduled reminders for PRN
if freq == "as_needed":
continue
# Day-of-week check for specific_days
if freq == "specific_days":
med_days = med.get("days_of_week", [])
if current_day not in med_days:
continue
# Interval check for every_n_days
if freq == "every_n_days":
start = med.get("start_date")
interval = med.get("interval_days")
if start and interval:
start_d = (
start
if isinstance(start, date_type)
else datetime.strptime(str(start), "%Y-%m-%d").date()
)
if (today - start_d).days < 0 or (
today - start_d
).days % interval != 0:
continue
else:
continue
# Time check
times = med.get("times", [])
if current_time not in times:
continue
# Already taken today? Check by created_at date
logs = postgres.select(
"med_logs", where={"medication_id": med["id"], "action": "taken"}
)
already_taken = any(
log.get("scheduled_time") == current_time
and str(log.get("created_at", ""))[:10] == today_str
for log in logs
)
if already_taken:
continue
user_settings = notifications.getNotificationSettings(user_uuid)
if user_settings:
msg = f"Time to take {med['name']} ({med['dosage']} {med['unit']}) · {current_time}"
notifications._sendToEnabledChannels(
user_settings, msg, user_uuid=user_uuid
)
except Exception as e:
logger.error(f"Error checking medication reminders: {e}")
def check_routine_reminders():
"""Check for scheduled routines due now and send notifications."""
try:
schedules = postgres.select("routine_schedules", where={"remind": True})
for schedule in schedules:
routine = postgres.select_one("routines", {"id": schedule["routine_id"]})
if not routine:
continue
now = _user_now_for(routine["user_uuid"])
current_time = now.strftime("%H:%M")
current_day = now.strftime("%a").lower()
if current_time != schedule.get("time"):
continue
days = schedule.get("days", [])
if current_day not in days:
continue
user_settings = notifications.getNotificationSettings(routine["user_uuid"])
if user_settings:
msg = f"Time to start your routine: {routine['name']}"
notifications._sendToEnabledChannels(
user_settings, msg, user_uuid=routine["user_uuid"]
)
except Exception as e:
logger.error(f"Error checking routine reminders: {e}")
def check_refills():
"""Check for medications running low on refills."""
try:
meds = postgres.select("medications")
for med in meds:
qty = med.get("quantity_remaining")
if qty is not None and qty <= 7:
user_settings = notifications.getNotificationSettings(med["user_uuid"])
if user_settings:
msg = f"Low on {med['name']}: only {qty} doses remaining. Time to refill!"
notifications._sendToEnabledChannels(
user_settings, msg, user_uuid=med["user_uuid"]
)
except Exception as e:
logger.error(f"Error checking refills: {e}")
def poll_callback():
"""Called every POLL_INTERVAL seconds."""
check_medication_reminders()
check_routine_reminders()
check_refills()
def daemon_loop():
logger.info("Scheduler daemon starting")
while True:
try:
poll_callback()
except Exception as e:
logger.error(f"Poll callback error: {e}")
time.sleep(POLL_INTERVAL)
if __name__ == "__main__":
daemon_loop()