import time
import urequests
import machine
import ujson
import dht
from onewire import OneWire
from ds18x20 import DS18X20
import neopixel
import bme280
from machine import ADC, Pin
import network
import math

# Configuration and Constants # =====================================================================================================================================================================

WIFI_SSID = "LITTLE 4795"
WIFI_PASSWORD = "U552;m84"

THINGSPEAK_API_KEY = "OL1QF3XH6G6BALNF"
THINGSPEAK_URL = f"https://api.thingspeak.com/update?api_key=OL1QF3XH6G6BALNF&field1=0"

DHT_PIN = 4
DS18B20_PIN = 5
NEOPIXEL_PIN = 2
I2C_SCL_PIN = 15
I2C_SDA_PIN = 14
UV_SENSOR_PIN = 28

NEOPIXEL_COUNT = 12
UV_LED_RANGE = range(0, 6)
TEMP_LED_RANGE = range(6, 12)

THINGSPEAK_DELAY = 1

# Initialization Functions # ========================================================================================================================================================================

def connect_wifi(ssid, password):
    """Connect to a WiFi network."""
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    if not wlan.isconnected():
        print(f"Connecting to WiFi SSID: {ssid}")
        wlan.connect(ssid, password)
        timeout = 20
        while not wlan.isconnected() and timeout > 0:
            print("Waiting for connection...")
            time.sleep(1)
            timeout -= 1
    if wlan.isconnected():
        print("Connected to WiFi")
        print("Network Configuration:", wlan.ifconfig())
    else:
        print("Failed to connect to WiFi")

def initialize_sensors():
    """Initialize all sensors."""
    global dht_sensor, ds_sensor, ds_roms, np, bme, uv_sensor

    dht_sensor = dht.DHT22(Pin(DHT_PIN))

    ow = OneWire(Pin(DS18B20_PIN))
    ds_sensor = DS18X20(ow)
    ds_roms = ds_sensor.scan()
    if not ds_roms:
        print("Warning: No DS18B20 devices found.")

    np = neopixel.NeoPixel(Pin(NEOPIXEL_PIN), NEOPIXEL_COUNT)

    try:
        i2c_2 = machine.I2C(1, scl=Pin(I2C_SCL_PIN), sda=Pin(I2C_SDA_PIN), freq=40000)
        bme = bme280.BME280(i2c=i2c_2)
    except Exception as e:
        print("Error initializing BME280:", e)
        bme = None

    try:
        uv_sensor = ADC(Pin(UV_SENSOR_PIN))
        print("UV sensor initialized.")
    except Exception as e:
        print("Error initializing UV sensor:", e)
        uv_sensor = None

# Utility Functions # ===============================================================================================================================================================================

def adjust_brightness(color, factor):
    """Adjust the brightness of an RGB color by a given factor (0 to 1)."""
    return tuple(int(c * factor) for c in color)

def set_neopixel_color_breathing(led_range, base_color, duration=2, steps=50):
    """Apply breathing effect to the given LED range with a specified base color."""
    # Calculate time per step
    time_per_step = duration / steps / 2

    # Breathe in
    for step in range(steps):
        factor = math.sin(math.pi * step / (2 * steps))
        dimmed_color = adjust_brightness(base_color, factor)
        for i in led_range:
            np[i] = dimmed_color
        np.write()
        time.sleep(time_per_step)

    # Breathe out
    for step in range(steps, 0, -1):
        factor = math.sin(math.pi * step / (2 * steps))
        dimmed_color = adjust_brightness(base_color, factor)
        for i in led_range:
            np[i] = dimmed_color
        np.write()
        time.sleep(time_per_step)

def set_neopixel_color_uv(uv_intensity):
    """Set NeoPixel colors for UV indicators based on UV intensity with breathing effect."""
    if uv_intensity is None:
        color = (0, 0, 0)
    elif uv_intensity > 0.4:
        color = (255, 0, 0)
    elif uv_intensity > 0.25:
        color = (255, 255, 0)
    else:
        color = (255, 0, 255)

    set_neopixel_color_breathing(UV_LED_RANGE, color)

def set_neopixel_color_temp(temperature):
    """Set NeoPixel colors for Temperature indicators based on temperature with breathing effect."""
    if temperature is None:
        color = (0, 0, 0)
    elif 0 <= temperature <= 19:
        color = (0, 255, 0)
    elif 20 <= temperature <= 29:
        color = (255, 255, 0)
    elif temperature >= 30:
        color = (255, 0, 0)
    else:
        color = (0, 0, 255)

    set_neopixel_color_breathing(TEMP_LED_RANGE, color)

def read_sensors():
    """Read all sensor data."""
    temperature, humidity = read_dht22()
    soil_temp = read_ds18b20()
    pressure = read_bme280()
    uv_level = read_uv_sensor()
    return temperature, humidity, soil_temp, pressure, uv_level

def read_dht22():
    """Read temperature and humidity from DHT22 sensor."""
    try:
        dht_sensor.measure()
        temperature = dht_sensor.temperature()
        humidity = dht_sensor.humidity()
        return temperature, humidity
    except OSError as e:
        print("DHT22 read error:", e)
        return None, None

def read_ds18b20():
    """Read temperature from DS18B20 sensor."""
    if not ds_roms:
        return None
    try:
        ds_sensor.convert_temp()
        time.sleep(1)
        soil_temp = ds_sensor.read_temp(ds_roms[0])
        return soil_temp
    except Exception as e:
        print("DS18B20 read error:", e)
        return None

def read_bme280():
    """Read pressure from BME280 sensor."""
    if not bme:
        return None
    try:
        pressure = bme.pressure
        return pressure
    except Exception as e:
        print("BME280 read error:", e)
        return None

def read_uv_sensor():
    """Read UV level from UV sensor."""
    if not uv_sensor:
        return None
    try:
        uv_value = uv_sensor.read_u16()
        uv_percentage = (uv_value / 65535) * 100
        return uv_percentage
    except Exception as e:
        print("UV sensor read error:", e)
        return None

def send_thingspeak(temperature, humidity, soil_temp, pressure, uv_level):
    """Send sensor data to ThingSpeak."""
    try:
        data = {
            "api_key": THINGSPEAK_API_KEY
        }
        if temperature is not None:
            data["field6"] = temperature
        if humidity is not None:
            data["field2"] = humidity
        if soil_temp is not None:
            data["field5"] = soil_temp
        if pressure is not None:
            data["field4"] = pressure
        if uv_level is not None:
            data["field7"] = uv_level

        encoded_data = "&".join([f"{key}={value}" for key, value in data.items()])

        headers = {"Content-Type": "application/x-www-form-urlencoded"}
        response = urequests.post(THINGSPEAK_URL, data=encoded_data, headers=headers)
        if response.status_code == 200:
            print("Data successfully sent to ThingSpeak.")
        else:
            print(f"ThingSpeak HTTP Error: {response.status_code}")
        response.close()
    except Exception as e:
        print("Error sending data to ThingSpeak:", e)

# Main Loop # =======================================================================================================================================================================================

def main():
    """
    Main function managing the connection to WiFi,
    initialization of sensors, data collection, and transmission.
    """
    # Establish WiFi connection
    connect_wifi(WIFI_SSID, WIFI_PASSWORD)

    # Initialize connected sensors
    initialize_sensors()

    # Initialize data sent counter
    data_counter = 0

    # Start the data collection and transmission cycle
    try:
        print("Starting data collection and transmission loop...")
        while True:
            # Gather sensor data
            temperature, humidity, soil_temp, pressure, uv_level = read_sensors()

            # Display current sensor readings in a formatted output
            print(f"--- Measurement {data_counter + 1} ---")
            print(f"Room Temperature: {temperature}°C" if temperature is not None else "Room Temperature: N/A")
            print(f"Room Humidity: {humidity}%" if humidity is not None else "Room Humidity: N/A")
            print(f"Soil Temperature: {soil_temp}°C" if soil_temp is not None else "Soil Temperature: N/A")
            print(f"Atmospheric Pressure: {pressure} hPa" if pressure is not None else "Atmospheric Pressure: N/A")
            print(f"UV Intensity Level: {uv_level}%" if uv_level is not None else "UV Intensity Level: N/A")

            # Update NeoPixel colors based on sensor readings
            set_neopixel_color_uv(uv_level)
            set_neopixel_color_temp(temperature)

            # Submit data to ThingSpeak and increment the counter
            send_thingspeak(temperature, humidity, soil_temp, pressure, uv_level)
            data_counter += 1
            print(f"Success: Data transmission count is now {data_counter}")

            # Wait before the next cycle to adhere to ThingSpeak's API rate limits
            time.sleep(THINGSPEAK_DELAY)
    except KeyboardInterrupt:
        # Graceful shutdown on user interrupt
        print("Program shutdown initiated by user.")
    except Exception as e:
        # Log unexpected errors elegantly
        print(f"Error: An unexpected problem occurred: {e}")

if __name__ == "__main__":
    main()

make connection faster and easier