from machine import Pin, PWM, I2C
from lcd import LCD
import dht
import time
import network
import urequests

# CONFIG
SSID     = "" # Your network name
PASSWORD = "" # Your network passowrd
LAT      = 37.7793  # Your latitude
LON      = -122.4193 # Your longitude

MARGIN           = 1.0   # degC - Accounts for poor accuracy on DHT11 (+/-2C)
SUSTAIN_MINUTES  = 5     # consecutive minutes before chime prevents flip-flop
READING_INTERVAL = 60    # seconds between readings
MUGGY_HUMIDITY   = 90    # don't suggest opening windows if high humidity outside
QUIET_START      = 21    # no chimes from this hour
QUIET_END        = 7     # until this hour

# ─── HARDWARE SETUP ───────────────────────────────
i2c    = I2C(0, sda=Pin(21), scl=Pin(22), freq=400000)
lcd    = LCD(i2c, 0x27, 2, 16)
sensor = dht.DHT11(Pin(4))
buzzer = PWM(Pin(18))
buzzer.duty(0)   # Removal of long startup beep

time.sleep(1)
lcd.clear()
time.sleep(0.5)

# SOUNDS
def play_note(freq, duration, duty=100):
    if freq == 0:
        buzzer.duty(0)
    else:
        buzzer.freq(freq)
        buzzer.duty(duty)
    time.sleep(duration)
    buzzer.duty(0)
    time.sleep(0.05)

def startup_chime():
    play_note(659, 0.08)
    play_note(880, 0.12)

def open_windows_chime():
    play_note(523, 0.12)
    play_note(659, 0.12)
    play_note(784, 0.12)
    play_note(1047, 0.25)

def close_windows_alert():
    for _ in range(2):
        play_note(880, 0.15)
        play_note(587, 0.25)
        time.sleep(0.15)

# QUIET HOURS
def is_quiet_hours():
    # Open-Meteo powered time... since ESP32 has no battery clock
    return local_hour >= QUIET_START or local_hour < QUIET_END

# WIFI
def connect_wifi():
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    wlan.connect(SSID, PASSWORD)
    lcd.clear()
    lcd.move_to(0, 0)
    lcd.putstr("Connecting WiFi")
    while not wlan.isconnected():
        time.sleep(0.5)
    lcd.clear()
    lcd.move_to(0, 0)
    lcd.putstr("Connected!")
    time.sleep(1)

# WEATHER
def get_outdoor():
    url = (
        f"https://api.open-meteo.com/v1/forecast"
        f"?latitude={LAT}&longitude={LON}"
        f"&current=temperature_2m,relative_humidity_2m"
        f"&timezone=Europe/London"
    )
    r = urequests.get(url)
    data = r.json()
    r.close()
    c = data['current']
    return (
        c['temperature_2m'],
        c['relative_humidity_2m'],
        int(c['time'][11:13]) # extracts time from response
    )

# DISPLAY
def recommendation(state, cooler_mins, warmer_mins, out_hum):
    quiet = is_quiet_hours()
    if state == "open":
        if warmer_mins >= SUSTAIN_MINUTES and quiet:
            return f"Close at {QUIET_END}am"
        elif warmer_mins > 0:
            return f"Close in {max(SUSTAIN_MINUTES - warmer_mins, 0)}min?"
        return "Keep open"
    else:
        if cooler_mins > 0 and out_hum >= MUGGY_HUMIDITY:
            return "Cooler but muggy"
        elif cooler_mins >= SUSTAIN_MINUTES and quiet:
            return f"Open at {QUIET_END}am :)"
        elif cooler_mins > 0:
            return f"Open in {max(SUSTAIN_MINUTES - cooler_mins, 0)}min?"
        return "Keep closed"

def update_display(in_temp, out_temp, state,
                   cooler_mins, warmer_mins, out_hum):
    lcd.clear()
    lcd.move_to(0, 0)
    lcd.putstr("In {:>2.0f}c Out{:>5.1f}c".format(in_temp, out_temp))
    lcd.move_to(0, 1)
    lcd.putstr(recommendation(state, cooler_mins, warmer_mins, out_hum))

def flash_message(top, bottom):
    lcd.clear()
    lcd.move_to(0, 0)
    lcd.putstr(top)
    lcd.move_to(0, 1)
    lcd.putstr(bottom)

# ─── MAIN ─────────────────────────────────────────
startup_chime()
connect_wifi()

state         = "closed"   # "closed" or "open"
cooler_mins   = 3          # consecutive minutes meaningfully cooler outside
warmer_mins   = 3          # consecutive minutes meaningfully warmer outside
first_reading = True       # decide immediately on boot, no sustain wait
local_hour    = 12         # safe default until the first API reading

in_temp = out_temp = out_hum = 0
next_reading = 0   # force immediate first read

while True:
    now = time.time()

    if now >= next_reading:
        try:
            sensor.measure()
            in_temp = sensor.temperature()

            out_temp, out_hum, local_hour = get_outdoor()

            diff = out_temp - in_temp

            if diff <= -MARGIN:
                cooler_mins += 1
                warmer_mins = 0
            elif diff >= MARGIN:
                warmer_mins += 1
                cooler_mins = 0
            else:
                cooler_mins = 0
                warmer_mins = 0

            if first_reading:
                if cooler_mins > 0:
                    cooler_mins = SUSTAIN_MINUTES
                elif warmer_mins > 0:
                    warmer_mins = SUSTAIN_MINUTES
                first_reading = False

            print(f"IN:{in_temp}C | OUT:{out_temp}C {out_hum}% | "
                  f"diff:{diff:+.1f} | cool:{cooler_mins} warm:{warmer_mins} | "
                  f"{state} | {local_hour}:00 quiet:{is_quiet_hours()}")

            if not is_quiet_hours():
                if (state == "closed" and
                        cooler_mins >= SUSTAIN_MINUTES and
                        out_hum < MUGGY_HUMIDITY):
                    flash_message("Cooler outside!", "Open windows")
                    open_windows_chime()
                    state = "open"
                    warmer_mins = 0
                    time.sleep(4)

                elif state == "open" and warmer_mins >= SUSTAIN_MINUTES:
                    flash_message("Warmer outside!", "Close windows")
                    close_windows_alert()
                    state = "closed"
                    cooler_mins = 0
                    time.sleep(4)

            update_display(in_temp, out_temp, state,
                           cooler_mins, warmer_mins, out_hum)

        except Exception as e:
            flash_message("Error!", "Retrying...")
            print(f"Error: {e}")

        next_reading = now + READING_INTERVAL

    time.sleep(1)
