// 
// EnergySensor.cpp
// 

#include <FS.h> // Zum Speichern des Überlauf-Wertes
#include "EnergySensor.h"
#include "UrsHelpers.h"

struct PZEMCommand {
  uint8_t command;
  uint8_t addr[4];
  uint8_t data;
  uint8_t crc;
};

#define PZEM_VOLTAGE (uint8_t)0xB0
#define RESP_VOLTAGE (uint8_t)0xA0

#define PZEM_CURRENT (uint8_t)0xB1
#define RESP_CURRENT (uint8_t)0xA1

#define PZEM_POWER   (uint8_t)0xB2
#define RESP_POWER   (uint8_t)0xA2

#define PZEM_ENERGY  (uint8_t)0xB3
#define RESP_ENERGY  (uint8_t)0xA3

#define PZEM_SET_ADDRESS (uint8_t)0xB4
#define RESP_SET_ADDRESS (uint8_t)0xA4

#define PZEM_POWER_ALARM (uint8_t)0xB5
#define RESP_POWER_ALARM (uint8_t)0xA5

#define RESPONSE_SIZE sizeof(PZEMCommand) 
#define RESPONSE_DATA_SIZE RESPONSE_SIZE - 2

#define PZEM_BAUD_RATE 9600

#define PZEM_ERROR_VALUE -1.0

EnergySensorClass::EnergySensorClass(uint8_t rxPin, uint8_t txPin, uint8_t coilTurns, uint16_t interval)
  : _pzemAddress(192, 168, 1, 1),_interval(interval), _CoilTurns(coilTurns) {
  _RxPin = rxPin;
  _TxPin = txPin;
}

bool EnergySensorClass::begin(float startValue, time_t startTime, Print* logDevice) {
  _LogDevice = logDevice;

  _StartValue = startValue; // Startwerte sichern
  _StartTime = startTime;

  if (!_serial)
     _serial = new PZEMSerial(_RxPin, _TxPin);
  _serial->begin(PZEM_BAUD_RATE);

  // Adresse festlegen (sicher ist sicher)
  if (!setAddress(_pzemAddress)) { // 1. Kommunikationsversuch
    delay(_ErrorDelay); // kleine Pause
    if (!setAddress(_pzemAddress)) { // noch ein Versuch
      _isRunning = false;
      if (_LogDevice) _LogDevice->println(F("Connection to PZEM-400T failed!"));
      return false;
    }
  }

  _isRunning = true;
  if (_LogDevice) _LogDevice->println(F("Connected to PZEM-400T"));
} // begin


void EnergySensorClass::handle(bool noDelay) {
  if (!noDelay)
     if (lastSensorRead + _interval * 1000 > millis()) 
        return; // keine Auswertung notwendig

  lastSensorRead = millis();

  static float lastEnergy = 0; // Für die Erkennung des Überlaufs
  EnergyData e;

  if (SystemTimeServer.getState() != UrsSystemTimeState::timeNotSet) {
    e.timeStamp = SystemTimeServer.now();
    e.secondsSinceStart = e.timeStamp - _StartTime;
    e.isTimeStampValid = true;
  } else {
    e.timeStamp = millis() / 1000;
  }

  if (!_serial) { // nicht initialisiert
    if (_LogDevice) _LogDevice->println(F("PZEM-400T: not initialized!"));
    lastReading = e;
    return;
  }

  if (!_isRunning) {    // Läuft nicht
    if(_LogDevice) _LogDevice->println(F("PZEM-400T: not running!"));
    lastReading = e;
    return;
  }

  if (_StartValue < 0) {  // Kein Basiswert
    if(_LogDevice) _LogDevice->println(F("PZEM-400T: no energy base!"));
    lastReading = e;
    return;
  }
  
  e.power = power(_pzemAddress) / _CoilTurns;
  if (e.power < 0.0) { // 2. Versuch
    if(_LogDevice) _LogDevice->println(F("PZEM-400T: Power retry"));
    delay(_ErrorDelay); // kleine Pause
    e.power = power(_pzemAddress) / _CoilTurns;
    if (e.power < 0.0) {
      if(_LogDevice) _LogDevice->println(F("PZEM-400T: invalid power reading!"));
      lastReading = e;
      return;
    }
  }

  e.energyRaw = energy(_pzemAddress) ;
  if (e.energyRaw < 0.0) { // 2. Versuch
    if(_LogDevice) _LogDevice->println(F("PZEM-400T: Energy retry"));
    delay(_ErrorDelay); // kleine Pause
    e.energyRaw = energy(_pzemAddress);
    if (e.energyRaw < 0.0) {
      if(_LogDevice) _LogDevice->println(F("PZEM-400T: invalid energy reading!"));
      lastReading = e;
      return;
    }
  }
  
  // Überlaufswert sichern
  if (e.energyRaw < lastEnergy) { // Überlauf
    SPIFFS.begin();
    File f = SPIFFS.open("/overflow.txt", "a");
    f.println(lastEnergy, 3);
    f.close();
  }
  lastEnergy = e.energyRaw;

  e.energy = (e.energyRaw - _StartValue) / _CoilTurns;
  e.isValid = true;

  lastReading = e;
} // getSensorData


void EnergySensorClass::reset() {
  if (!_serial) { // nicht initialisiert
    if (_LogDevice) _LogDevice->println(F("PZEM-400T: not initialized!"));
    return;
  }

  if (!_isRunning) { // Läuft nicht
    if (_LogDevice) _LogDevice->println(F("PZEM-400T: not running!"));
    return;
  }

  // Energie-Wert einlesen
  _StartValue = energy(_pzemAddress); // Energie-Null-Wert
  if (_StartValue < 0) { // 2. Versuch
    delay(_ErrorDelay); // kleine Pause
    if (_StartValue < 0) {
      if (_LogDevice) {
        _LogDevice->println(F("PZEM-400T: Cannot reset power value!"));
        return;
      }
    }
  }
  _StartValue = energy(_pzemAddress);
  _StartTime = SystemTimeServer.now();

  if (_SensorReset)
    _SensorReset(_StartValue, _StartTime);

  handle(true);
} // reset

bool EnergySensorClass::setAddress(const IPAddress &newAddr) {
  send(newAddr, PZEM_SET_ADDRESS);
  return recieve(RESP_SET_ADDRESS);
}

float EnergySensorClass::power(const IPAddress &addr) {
  uint8_t data[RESPONSE_DATA_SIZE];

  send(addr, PZEM_POWER);
  if (!recieve(RESP_POWER, data))
    return PZEM_ERROR_VALUE;

  return (data[0] << 8) + data[1];
} // power

float EnergySensorClass::energy(const IPAddress &addr) {
  uint8_t data[RESPONSE_DATA_SIZE];

  send(addr, PZEM_ENERGY);
  if (!recieve(RESP_ENERGY, data))
    return PZEM_ERROR_VALUE;

  return ((uint32_t)data[0] << 16) + ((uint16_t)data[1] << 8) + data[2];
} // energy

void EnergySensorClass::send(const IPAddress &addr, uint8_t cmd, uint8_t data) {
  PZEMCommand pzem;

  pzem.command = cmd;
  for (int i = 0; i<sizeof(pzem.addr); i++)
    pzem.addr[i] = addr[i];
  pzem.data = data;

  uint8_t *bytes = (uint8_t*)&pzem;
  pzem.crc = crc(bytes, sizeof(pzem) - 1);

  while (_serial->available())
    _serial->read();

  _serial->write(bytes, sizeof(pzem));
}

bool EnergySensorClass::recieve(uint8_t resp, uint8_t *data) {
  uint8_t buffer[RESPONSE_SIZE];

  _serial->enableRx(true);

  uint32_t startTime = millis();
  uint8_t len = 0;
  while ((len < RESPONSE_SIZE) && (millis() - startTime < _readTimeOut)) {
    if (_serial->available() > 0) {
      uint8_t c = (uint8_t)_serial->read();
      if (!c && !len)
        continue; // skip 0 at startup
      buffer[len++] = c;
    }
    yield();	// do background netw tasks while blocked for IO (prevents ESP watchdog trigger)
  }

  if (len != RESPONSE_SIZE) {
    if(_LogDevice) _LogDevice->println(F("PZEM-400T: Timeout"));
    return false;
  }

  if (buffer[6] != crc(buffer, len - 1)) {
    if(_LogDevice) _LogDevice->print(F("PZEM-400T: CRC ("));
    if(_LogDevice) _LogDevice->print(crc(buffer, len - 1), HEX);
    if(_LogDevice) _LogDevice->print(F("): "));
    for (uint8_t i = 0; i < len; i++)
      if(_LogDevice) _LogDevice->print(buffer[i], HEX);
    if(_LogDevice) _LogDevice->println(F(""));

    return false;
  }

  if (buffer[0] != resp) {
    if(_LogDevice) _LogDevice->print(F("PZEM-400T: buffer[0], expected: "));
    if(_LogDevice) _LogDevice->print(resp, HEX);
    if (_LogDevice) _LogDevice->print(F(" got: "));
    if (_LogDevice) _LogDevice->println(buffer[0], HEX);
    return false;
  }

  if (data) {
    for (int i = 0; i<RESPONSE_DATA_SIZE; i++)
      data[i] = buffer[1 + i];
  }

  return true;
}

uint8_t EnergySensorClass::crc(uint8_t *data, uint8_t sz) {
  uint16_t crc = 0;
  for (uint8_t i = 0; i<sz; i++)
    crc += *data++;
  return (uint8_t)(crc & 0xFF);
}


String EnergyData::toString() {
  String responseString;

  // Energie-Messwerte
  if (isValid) {
    responseString = ftoa(power, 1) + ";" + ftoa(energy / 1000, 4) + ";ok;";
  } else {
    responseString = "0;0;err;";
  }

  // Zeit
  responseString += secondsSinceStart;

  if (isTimeStampValid) {
    uint32_t t = secondsSinceStart / 60; // Volle Minuten
    uint8_t minutes = t % 60;
    t = t / 60; // Volle Stunden
    uint8_t hours = t % 24;
    uint16_t days = t / 24;

    char buffer[30];
    sprintf(buffer, ";%id %02ih %02im;ok", days, hours, minutes);
    responseString += buffer;
  } else
    responseString += ";Error;err";

  return responseString;
}

EnergySensorClass EnergySensor(PZEM_TX, PZEM_RX, COIL_TURNS);

