// ESP32-S2 RainMaker Smart Energy Meter - Separate Sensor Devices
// ACS712 current + ZMPT101B voltage
// Relays with protection + Sync force-report switch

#include "RMaker.h"
#include "WiFi.h"
#include "WiFiProv.h"

const char *service_name = "RainMaker_BLE";
const char *pop = "12345678";

// ────────────────────────────────────────────────
// Hardware & Calibration Constants
// ────────────────────────────────────────────────
const int SENSOR_PIN     = 4;           // ACS712 output
const int V_SENSOR_PIN   = 5;           // ZMPT101B output
const int RELAY1_PIN     = 6;
const int RELAY2_PIN     = 33;

const float ADC_VCC         = 3.3f;
const float SENSITIVITY     = 0.185f;   // V/A for ACS712-05B (change if different model)
const float V_SENSITIVITY   = 500.0f;   // mV per Volt - typical for many ZMPT101B modules (calibrate!)
const float DIVIDER_RATIO   = 22.0f / (10.0f + 22.0f);  // if used - often 1.0 for ZMPT
const float V_DIVIDER_RATIO = 1.0f;

const float NOISE_THRESHOLD_MA = 150.0f;
const float COST_PER_UNIT      = 0.15f;     // currency per kWh

// Protection thresholds
const float OVER_VOLTAGE  = 250.0f;
const float UNDER_VOLTAGE = 180.0f;
const float OVER_CURRENT  = 5.0f;

// Reporting
const unsigned long REPORT_INTERVAL = 3600000UL;  // 1 hour

// ────────────────────────────────────────────────
// Global state
// ────────────────────────────────────────────────
float OFFSET     = 1.65f;   // initial guess - calibrated later
float V_OFFSET   = 1.65f;

float totalEnergyWh = 0.0f;

unsigned long lastTime       = 0;
unsigned long lastReportTime = 0;

bool relay1_state = false;
bool relay2_state = false;
bool forceReport  = false;

float latest_vrms  = 0.0f;
float latest_irms  = 0.0f;
float latest_power = 0.0f;
float latest_freq  = 0.0f;

// ────────────────────────────────────────────────
// RainMaker Devices
// ────────────────────────────────────────────────
static Device voltage_sensor("Voltage Sensor", NULL, NULL);
static Device current_sensor("Current Sensor", NULL, NULL);
static Device power_sensor  ("Power Sensor",   NULL, NULL);
static Device freq_sensor   ("Frequency Sensor", NULL, NULL);
static Device energy_sensor ("Energy Usage",   NULL, NULL);
static Device bill_sensor   ("Bill Calculator", NULL, NULL);

static Switch relay1_switch("Relay 1", NULL);
static Switch relay2_switch("Relay 2", NULL);
static Switch sync_switch  ("Sync",    NULL);

// ────────────────────────────────────────────────
// Write callback - handles relay commands & sync
// ────────────────────────────────────────────────
void write_callback(Device *device, Param *param, const param_val_t val, void *priv_data, write_ctx_t *ctx) {
    const char *dev_name  = device->getDeviceName();
    const char *param_name = param->getParamName();

    if (strcmp(param_name, "Power") != 0) return;

    bool new_state = val.val.b;
    Serial.printf("Power command: %s → %s\n", dev_name, new_state ? "ON" : "OFF");

    if (strcmp(dev_name, "Relay 1") == 0 || strcmp(dev_name, "Relay 2") == 0) {
        if (new_state) {  // only check protection when turning ON
            if (latest_vrms > OVER_VOLTAGE || latest_vrms < UNDER_VOLTAGE || latest_irms > OVER_CURRENT) {
                Serial.println("Protection active → forcing OFF");
                new_state = false;
            }
        }

        if (strcmp(dev_name, "Relay 1") == 0) {
            relay1_state = new_state;
            digitalWrite(RELAY1_PIN, relay1_state ? HIGH : LOW);
        } else {
            relay2_state = new_state;
            digitalWrite(RELAY2_PIN, relay2_state ? HIGH : LOW);
        }

        param->updateAndReport(value(new_state));
    }
    else if (strcmp(dev_name, "Sync") == 0) {
        if (new_state) {
            forceReport = true;
            Serial.println("Sync triggered → forcing report");
            param->updateAndReport(value(false));  // reset switch to OFF
        }
    }
}

// ────────────────────────────────────────────────
// Provisioning QR/event
// ────────────────────────────────────────────────
void sysProvEvent(arduino_event_t *sys_event) {
    switch (sys_event->event_id) {
        case ARDUINO_EVENT_PROV_START:
#if CONFIG_IDF_TARGET_ESP32S2
            Serial.printf("Provisioning started: %s  PoP: %s  (SoftAP)\n", service_name, pop);
            printQR(service_name, pop, "softap");
#else
            Serial.printf("Provisioning started: %s  PoP: %s  (BLE)\n", service_name, pop);
            printQR(service_name, pop, "ble");
#endif
            break;
        default: break;
    }
}

// ────────────────────────────────────────────────
// Calibrate ADC midpoint (no load)
// ────────────────────────────────────────────────
float calibrateOffset(int pin, float divider) {
    long sum = 0;
    const int samples = 1200;
    for (int i = 0; i < samples; i++) {
        sum += analogRead(pin);
        delayMicroseconds(80);
    }
    float avg = sum / (float)samples;
    float volts = (avg / 4095.0f) * ADC_VCC;
    return volts / divider;
}

// ────────────────────────────────────────────────
// Measure voltage, current, power, frequency
// ────────────────────────────────────────────────
void measureAll(float &vrms, float &irms, float &power, float &freq) {
    float sumV2 = 0, sumI2 = 0, sumP = 0;
    int crossings = 0;
    float lastSign = 0;
    const int SAMPLES = 1800;
    unsigned long t_start = micros();

    for (int i = 0; i < SAMPLES; i++) {
        int rawV = analogRead(V_SENSOR_PIN);
        int rawI = analogRead(SENSOR_PIN);

        float v_adc = rawV * (ADC_VCC / 4095.0f);
        float v_inst = (v_adc / V_DIVIDER_RATIO) - V_OFFSET;

        float i_adc = rawI * (ADC_VCC / 4095.0f);
        float i_inst = ((i_adc / DIVIDER_RATIO) - OFFSET) * SENSITIVITY;  // Amps

        sumV2 += v_inst * v_inst;
        sumI2 += i_inst * i_inst;
        sumP  += v_inst * i_inst;

        float sign = (v_inst > 0) ? 1.0f : (v_inst < 0 ? -1.0f : 0.0f);
        if (i > 0 && sign != 0 && lastSign != 0 && sign != lastSign) {
            crossings++;
        }
        lastSign = sign;

        delayMicroseconds(60);
    }

    unsigned long duration_us = micros() - t_start;

    float period_us = (crossings > 1) ? (duration_us / (float)crossings) : 0;
    freq = (period_us > 0) ? (1000000.0f / period_us) : 50.0f;

    vrms  = sqrt(sumV2 / SAMPLES) * V_SENSITIVITY;
    irms  = sqrt(sumI2 / SAMPLES);
    power = sumP / SAMPLES * V_SENSITIVITY;

    if (irms * 1000.0f < NOISE_THRESHOLD_MA) irms = 0.0f;
}

// ────────────────────────────────────────────────
// SETUP
// ────────────────────────────────────────────────
void setup() {
    Serial.begin(115200);
    delay(300);
    Serial.println("\nSmart Meter starting...");

    // Calibrate offsets (important!)
    Serial.println("Calibrating offsets...");
    OFFSET   = calibrateOffset(SENSOR_PIN,   DIVIDER_RATIO);
    V_OFFSET = calibrateOffset(V_SENSOR_PIN, V_DIVIDER_RATIO);
    Serial.printf("  OFFSET = %.3f V   V_OFFSET = %.3f V\n", OFFSET, V_OFFSET);

    pinMode(RELAY1_PIN, OUTPUT);
    pinMode(RELAY2_PIN, OUTPUT);
    digitalWrite(RELAY1_PIN, LOW);
    digitalWrite(RELAY2_PIN, LOW);

    Node node = RMaker.initNode("Indus Energy Meter");

    // ── Sensor params (float, read-only) ──
    Param p_v("Voltage",   NULL, value(0.0f), PROP_FLAG_READ);
    p_v.addUIType(ESP_RMAKER_UI_TEXT);
    voltage_sensor.addParam(p_v);

    Param p_i("Current",   NULL, value(0.0f), PROP_FLAG_READ);
    p_i.addUIType(ESP_RMAKER_UI_TEXT);
    current_sensor.addParam(p_i);

    Param p_p("Power",     NULL, value(0.0f), PROP_FLAG_READ);
    p_p.addUIType(ESP_RMAKER_UI_TEXT);
    power_sensor.addParam(p_p);

    Param p_f("Frequency", NULL, value(0.0f), PROP_FLAG_READ);
    p_f.addUIType(ESP_RMAKER_UI_TEXT);
    freq_sensor.addParam(p_f);

    Param p_e("Energy",    NULL, value(0.0f), PROP_FLAG_READ);
    p_e.addUIType(ESP_RMAKER_UI_TEXT);
    energy_sensor.addParam(p_e);

    Param p_b("Bill",      NULL, value(0.0f), PROP_FLAG_READ);
    p_b.addUIType(ESP_RMAKER_UI_TEXT);
    bill_sensor.addParam(p_b);

    node.addDevice(voltage_sensor);
    node.addDevice(current_sensor);
    node.addDevice(power_sensor);
    node.addDevice(freq_sensor);
    node.addDevice(energy_sensor);
    node.addDevice(bill_sensor);

    // Switches
    relay1_switch.addCb(write_callback);
    relay2_switch.addCb(write_callback);
    sync_switch.addCb(write_callback);

    node.addDevice(relay1_switch);
    node.addDevice(relay2_switch);
    node.addDevice(sync_switch);

    RMaker.enableOTA(OTA_USING_PARAMS);
    RMaker.enableTZService();
    RMaker.start();

    WiFi.onEvent(sysProvEvent);

#if CONFIG_IDF_TARGET_ESP32S2
    WiFiProv.beginProvision(WIFI_PROV_SCHEME_SOFTAP, WIFI_PROV_SCHEME_HANDLER_NONE, WIFI_PROV_SECURITY_1, pop, service_name);
#else
    WiFiProv.beginProvision(WIFI_PROV_SCHEME_BLE, WIFI_PROV_SCHEME_HANDLER_FREE_BTDM, WIFI_PROV_SECURITY_1, pop, service_name);
#endif

    lastTime = lastReportTime = millis();
    Serial.println("Setup done. Provision if needed.");
}

// ────────────────────────────────────────────────
// LOOP
// ────────────────────────────────────────────────
void loop() {
    float vrms, irms, power, freq;
    measureAll(vrms, irms, power, freq);

    latest_vrms  = vrms;
    latest_irms  = irms;
    latest_power = power;
    latest_freq  = freq;

    unsigned long now = millis();
    float dt_hours = (now - lastTime) / 3600000.0f;
    lastTime = now;

    totalEnergyWh += power * dt_hours;
    float totalKWh = totalEnergyWh / 1000.0f;
    float bill = totalKWh * COST_PER_UNIT;

    // Protection logic
    bool fault = (vrms > OVER_VOLTAGE || vrms < UNDER_VOLTAGE || irms > OVER_CURRENT);
    if (fault) {
        if (relay1_state) {
            relay1_state = false;
            digitalWrite(RELAY1_PIN, LOW);
            relay1_switch.updateAndReportParam(ESP_RMAKER_DEF_POWER_NAME, false);
            Serial.println("Protection → Relay 1 OFF");
        }
        if (relay2_state) {
            relay2_state = false;
            digitalWrite(RELAY2_PIN, LOW);
            relay2_switch.updateAndReportParam(ESP_RMAKER_DEF_POWER_NAME, false);
            Serial.println("Protection → Relay 2 OFF");
        }
    }

    // Reporting logic
    if (forceReport || (now - lastReportTime >= REPORT_INTERVAL)) {
        voltage_sensor.updateAndReportParam("Voltage",   vrms);
        current_sensor.updateAndReportParam("Current",   irms);
        power_sensor.updateAndReportParam  ("Power",     power);
        freq_sensor.updateAndReportParam   ("Frequency", freq);
        energy_sensor.updateAndReportParam ("Energy",    totalKWh);
        bill_sensor.updateAndReportParam   ("Bill",      bill);

        lastReportTime = now;
        forceReport = false;
        Serial.println("Reported all values");
    }

    delay(900);  // ~1 Hz loop
}