"""
Raspberry Pi that sends a notification signaling cold water.
-------------------
Reads a DS18B20 temperature sensor taped to a water bottle in the fridge.
Sends a phone notification once the water has cooled as far
as it's going to and stopped dropping.
"""

import time
import glob
import requests
from datetime import datetime
from collections import deque


NTFY_TOPIC = "coldwater-alerts-ethan-2026"   #NTFY title
CHECK_INTERVAL = 60          # seconds between temperature readings
WINDOW_MINUTES = 15          # how far back to look when judging "has it stopped cooling"
RATE_THRESHOLD = 0.2         # degrees C change over the window that counts as "flat"
STABLE_CHECKS_NEEDED = 3     # how many checks in a row must be "flat" before alerting
NEW_BOTTLE_JUMP = 3.0        # a temp rise bigger than this means a new bottle went in

def find_sensor_path():
    """Locate the DS18B20 sensor's data file automatically."""
    matches = glob.glob("/sys/bus/w1/devices/28-*/w1_slave")
    if not matches:
        raise RuntimeError(
            "No DS18B20 sensor found under /sys/bus/w1/devices/. "
            "Check wiring and that 1-Wire is enabled (raspi-config)."
        )
    return matches[0]


def read_temperature(sensor_path):
    """Read and parse the current temperature in Celsius. Returns None on a bad reading."""
    with open(sensor_path, "r") as f:
        lines = f.readlines()

    # First line ends with YES if the checksum passed
    if len(lines) < 2 or "YES" not in lines[0]:
        return None

    # Second line contains the temp (millidegrees C)
    temp_marker = lines[1].find("t=")
    if temp_marker == -1:
        return None

    temp_c = float(lines[1][temp_marker + 2:]) / 1000.0
    return temp_c


def send_notification(title, message):
    """This pushes the notification to ntfy, which will then alert your phone"""
    try:
        requests.post(
            f"https://ntfy.sh/{NTFY_TOPIC}",
            data=message.encode("utf-8"),
            headers={"Title": title},
            timeout=10,
        )
        print(f"[{datetime.now().strftime('%H:%M:%S')}] Notification sent: {title}")
    except requests.RequestException as e:
        print(f"[{datetime.now().strftime('%H:%M:%S')}] Failed to send notification: {e}")


def main():
    sensor_path = find_sensor_path()
    print(f"Using sensor: {sensor_path}")
    print(f"Notification topic: {NTFY_TOPIC}")
    print("Starting monitor loop. Press Ctrl+C to stop.\n")

    readings = deque()

    watching_for_cold = True   # False once we've already alerted, until a new bottle appears
    stable_count = 0
    last_temp = None

    while True:
        temp = read_temperature(sensor_path)
        now = datetime.now()

        if temp is None:
            print(f"[{now.strftime('%H:%M:%S')}] Bad reading, skipping this check.")
            time.sleep(CHECK_INTERVAL)
            continue

        print(f"[{now.strftime('%H:%M:%S')}] Temp: {temp:.2f} C")

        # Detect a new, warmer bottle being put in -> reset and start watching again
        if last_temp is not None and (temp - last_temp) > NEW_BOTTLE_JUMP:
            print(f"[{now.strftime('%H:%M:%S')}] Temperature jumped up — looks like a new bottle. Resetting.")
            readings.clear()
            stable_count = 0
            watching_for_cold = True

        last_temp = temp
        readings.append((now, temp))

        # Drop readings older than our window
        cutoff = now.timestamp() - (WINDOW_MINUTES * 60)
        while readings and readings[0][0].timestamp() < cutoff:
            readings.popleft()

        if watching_for_cold and len(readings) >= 2:
            oldest_temp = readings[0][1]
            newest_temp = readings[-1][1]
            drop = oldest_temp - newest_temp

            span_minutes = (readings[-1][0] - readings[0][0]).total_seconds() / 60
            if span_minutes >= WINDOW_MINUTES - 1:  # only judge once we have a full window
                if drop < RATE_THRESHOLD:
                    stable_count += 1
                    print(f"    -> Cooling has flattened ({stable_count}/{STABLE_CHECKS_NEEDED} checks)")
                else:
                    stable_count = 0

                if stable_count >= STABLE_CHECKS_NEEDED:
                    send_notification(
                        "Your water is cold!",
                        f"Bottle has settled at {newest_temp:.1f} C. Go grab it!"
                    )
                    watching_for_cold = False  # wait for a new bottle before alerting again

        time.sleep(CHECK_INTERVAL)


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\nStopped.")
