/*
  xsns_92_scd40.ino - SCD40/SCD41 I2C CO2(+temp+RH) sensor support for Tasmota,
                      based on frogmore42's xsns_42_scd30.ino

  Copyright (C) 2021  Frogmore42, Arnold-n

  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program.  If not, see <http://www.gnu.org/licenses/>.

*/

// define USE_SCD40 to use SCD40 or SCD41 device; use SCD41-functions only if you use SCD41 sensor
// define USE_SCD40_LOWPOWER to use low-power periodic measurement mode (both SCD40 and SCD41)
// define SCD40_DEBUG to debug

// Console instructions supported: (errorvalue=-1 in case of error, errorvalue=0 otherwise)
//                                 (data=-1 in case of error, value otherwise)
//                                 (third colum: time in ms needed for execution)
//                                 (DPM: may be executed during periodic measurements)
//
// Instruction    Returns   Exec(ms) DPM Function
// ------------------------------------------------------------------------------------
// SCD40Alt       data            1  no  get Sensor Altitude (in m)
// SCD40Alt x     errorvalue      1  no  set Sensor Altitude (in m)
// SCD40Auto      data            1  no  get CalibrationEnabled status (bool)
// SCD40Auto x    errorvalue      1  no  set CalibrationEnabled status (bool)
// SCD40Toff      data            1  no  get Temperature offset (centigrades)
// SCD40Toff x    errorvalue      1  no  set Temperature offset (centigrades) (some rounding may occur)
// SCD40Pres x    errorvalue      1  yes set Ambient Pressure (mbar) (overrides Sensor Altitude setting)
// SCD40Cal x     errorvalue    400  no  perform forced recalibration (ppm CO2)
// SCD40Test      errorvalue  10000  no  perform selftest
// SCD40StLp      errorvalue      0  no  start periodic measurement in low-power mode (1/30s)
// SCD40Strt      errorvalue      0  no  start periodic measurement (1/5s)
// SCD40Stop      errorvalue    500  yes stop periodic measurement
// SCD40Pers      errorvalue    800  no  persist settings in EEPROM (2000 write cycles guaranteed)
// SCD40Rein      errorvalue     20  no  reinit sensor
// SCD40Fact      errorvalue   1200  no  factory reset sensor
//
// SCD40Sing      errorvalue   5000  no  (SCD41 only) measure single shot
// SCD40SRHT      errorvalue     50  no  (SCD41 only) measure single shot, RHT only

//#define SCD40_DEBUG

#ifdef USE_I2C

#ifdef USE_SCD40

#define XSNS_92        92
#define XI2C_62        62  // See I2CDEVICES.md

// #define SCD40_ADDRESS  0x62 // already defined in lib

#define SCD40_MAX_MISSED_READS 30 // in seconds (at 1 read/second)
#define SCD40_STATE_NO_ERROR 0
#define SCD40_STATE_ERROR_DATA_CRC 1
#define SCD40_STATE_ERROR_READ_MEAS 2
#define SCD40_STATE_ERROR_SOFT_RESET 3
#define SCD40_STATE_ERROR_I2C_RESET 4

#include "Arduino.h"
#include <FrogmoreScd40.h>

#define D_CMND_SCD40 "SCD40"

const char S_JSON_SCD40_COMMAND_NVALUE[] PROGMEM = "{\"" D_CMND_SCD40 "%s\":%d}";
//const char S_JSON_SCD40_COMMAND[] PROGMEM        = "{\"" D_CMND_SCD40 "%s\"}";
const char kSCD40_Commands[] PROGMEM             = "Alt|Auto|Toff|Pres|Cal|Test|StLP|Strt|Stop|Pers|Rein|Fact|Sing|SRHT";

/*********************************************************************************************\
 * enumerations    
\*********************************************************************************************/

enum SCD40_Commands {         // commands useable in console or rules
  CMND_SCD40_ALTITUDE,
  CMND_SCD40_AUTOMODE,
  CMND_SCD40_TEMPOFFSET,
  CMND_SCD40_PRESSURE,
  CMND_SCD40_FORCEDRECALIBRATION,
  CMND_SCD40_SELFTEST,
  CMND_SCD40_START_MEASUREMENT_LOW_POWER,
  CMND_SCD40_START_MEASUREMENT,
  CMND_SCD40_STOP_MEASUREMENT,
  CMND_SCD40_PERSIST,
  CMND_SCD40_REINIT,
  CMND_SCD40_FACTORYRESET,
  CMND_SCD40_SINGLESHOT,
  CMND_SCD40_SINGLESHOT_RHT_ONLY
};

FrogmoreScd40 scd40;

bool scd40Found = false;
bool scd40IsDataValid = false;
int scd40ErrorState = SCD40_STATE_NO_ERROR;
int scd40Loop_count = 0;
int scd40DataNotAvailable_count = 0;
int scd40GoodMeas_count = 0;
int scd40Reset_count = 0;
int scd40CrcError_count = 0;
int scd40i2cReset_count = 0;
uint16_t scd40_CO2 = 0;
uint16_t scd40_CO2EAvg = 0;
float scd40_Humid = 0.0;
float scd40_Temp = 0.0;

void Scd40Detect(void)
{
  if (!I2cSetDevice(SCD40_ADDRESS)) { return; }

  scd40.begin();

  // don't stop in case of error, try to continue
  delay(10); // not sure whether this is needed
  int error = scd40.forceStopPeriodicMeasurement();  // after reboot, stop (if any) periodic measurement, or reinit may not work
#ifdef SCD40_DEBUG
  AddLog(LOG_LEVEL_DEBUG, PSTR("SCD40 force-stop error: %d"), error);
#endif
  delay(550); // wait >500ms after stopPeriodicMeasurement before SCD40 allows any other command

  error = scd40.reinit(); // just in case
#ifdef SCD40_DEBUG
  AddLog(LOG_LEVEL_DEBUG, PSTR("SCD40 reinit error: %d"), error);
#endif
  delay(20); // not sure whether this is needed

  uint16_t sn[3];
  error = scd40.getSerialNumber(sn);
  AddLog(LOG_LEVEL_NONE, PSTR("SCD40 serial nr 0x%X 0x%X 0x%X") ,sn[0], sn[1], sn[2]);

  //  by default, start measurements, only register device if this succeeds
#ifdef USE_SCD40_LOWPOWER
  if (scd40.startLowPowerPeriodicMeasurement()) { return; }
#else
  if (scd40.startPeriodicMeasurement()) { return; }
#endif
  I2cSetActiveFound(SCD40_ADDRESS, "SCD40");
  scd40Found = true;
#ifdef SCD40_DEBUG
  AddLog(LOG_LEVEL_DEBUG, PSTR("SCD40 found, measurements started."));
#endif
}

// gets data from the sensor
void Scd40Update(void)
{
  bool isAvailable;

  scd40Loop_count++;

  uint32_t error = 0;
  switch (scd40ErrorState) {
    case SCD40_STATE_NO_ERROR: {
      error = scd40.readMeasurement(&scd40_CO2, &scd40_CO2EAvg, &scd40_Temp, &scd40_Humid);
      switch (error) {
        case ERROR_SCD40_NO_ERROR:
          scd40Loop_count = 0;
          scd40IsDataValid = true;
          scd40GoodMeas_count++;
          break;

        case ERROR_SCD40_NO_DATA:
          scd40DataNotAvailable_count++;
          AddLog(LOG_LEVEL_DEBUG, PSTR("SCD40: no data available."));
          break;

        case ERROR_SCD40_CRC_ERROR:
          scd40CrcError_count++;
#ifdef SCD40_DEBUG
          AddLog(LOG_LEVEL_ERROR, PSTR("SCD40: CRC error, CRC error: %ld, good: %ld, no data: %ld, sc30_reset: %ld, i2c_reset: %ld"),
            scd40CrcError_count, scd40GoodMeas_count, scd40DataNotAvailable_count, scd40Reset_count, scd40i2cReset_count);
#endif
          break;

        default: {
          scd40ErrorState = SCD40_STATE_ERROR_READ_MEAS;
#ifdef SCD40_DEBUG
          AddLog(LOG_LEVEL_ERROR, PSTR("SCD40: Update: ReadMeasurement error: 0x%lX, counter: %ld"), error, scd40Loop_count);
#endif
          return;
        }
        break;
      }
    }
    break;

    case SCD40_STATE_ERROR_READ_MEAS: {
#ifdef SCD40_DEBUG
      AddLog(LOG_LEVEL_ERROR, PSTR("SCD40: (rd) in error state: %d, good: %ld, no data: %ld, sc30 reset: %ld, i2c reset: %ld"),
        scd40ErrorState, scd40GoodMeas_count, scd40DataNotAvailable_count, scd40Reset_count, scd40i2cReset_count);
      AddLog(LOG_LEVEL_ERROR, PSTR("SCD40: not answering, sending soft reset, counter: %ld"), scd40Loop_count);
#endif
      scd40Reset_count++;
      error = scd40.stopPeriodicMeasurement();
      if (error) {
        scd40ErrorState = SCD40_STATE_ERROR_SOFT_RESET;
#ifdef SCD40_DEBUG
        AddLog(LOG_LEVEL_ERROR, PSTR("SCD40: stopPeriodicMeasurement got error: 0x%lX"), error);
#endif
      } else {
        error = scd40.reinit();
        if (error) {
#ifdef SCD40_DEBUG
          AddLog(LOG_LEVEL_ERROR, PSTR("SCD40: resetting got error: 0x%lX"), error);
#endif
          scd40ErrorState = SCD40_STATE_ERROR_SOFT_RESET;
        } else {
          scd40ErrorState = ERROR_SCD40_NO_ERROR;
        }
      }
    }
    break;

    case SCD40_STATE_ERROR_SOFT_RESET: {
#ifdef SCD40_DEBUG
      AddLog(LOG_LEVEL_ERROR, PSTR("SCD40: (rst) in error state: %d, good: %ld, no data: %ld, sc30 reset: %ld, i2c reset: %ld"),
        scd40ErrorState, scd40GoodMeas_count, scd40DataNotAvailable_count, scd40Reset_count, scd40i2cReset_count);
      AddLog(LOG_LEVEL_ERROR, PSTR("SCD40: clearing i2c bus"));
#endif
      scd40i2cReset_count++;
      error = scd40.clearI2CBus();
      if (error) {
        scd40ErrorState = SCD40_STATE_ERROR_I2C_RESET;
#ifdef SCD40_DEBUG
        AddLog(LOG_LEVEL_ERROR, PSTR("SCD40: error clearing i2c bus: 0x%lX"), error);
#endif
      } else {
        scd40ErrorState = ERROR_SCD40_NO_ERROR;
      }
    }
    break;

    case SCD40_STATE_ERROR_I2C_RESET: {
      // Give up
    }
    break;
  }

  if (scd40Loop_count > SCD40_MAX_MISSED_READS) {
    AddLog(LOG_LEVEL_DEBUG, PSTR("SCD40: max-missed-reads."));
    scd40IsDataValid = false;
  }
}

int Scd40GetCommand(int command_code, uint16_t *pvalue)
{
  switch (command_code)
  {
    case CMND_SCD40_ALTITUDE:
      return scd40.getSensorAltitude(pvalue);
      break;

    case CMND_SCD40_AUTOMODE:
      return scd40.getAutomaticSelfCalibrationEnabled(pvalue);
      break;

    case CMND_SCD40_TEMPOFFSET:
      return scd40.getTemperatureOffset(pvalue);
      break;

    case CMND_SCD40_SELFTEST:
      return scd40.performSelfTest(pvalue);
      break;

    default:
      // else for Unknown command
    break;
  }
  return 0;  // Fix GCC 10.1 warning
}

int Scd40SetCommand(int command_code, uint16_t *pvalue)
{
  switch (command_code)
  {
    case CMND_SCD40_ALTITUDE:
      return scd40.setSensorAltitude(*pvalue);
      break;

    case CMND_SCD40_AUTOMODE:
      return scd40.setAutomaticSelfCalibrationEnabled((bool) (*pvalue));
      break;

    case CMND_SCD40_TEMPOFFSET:
      return scd40.setTemperatureOffset(*pvalue);
      break;

    case CMND_SCD40_PRESSURE:
      return scd40.setAmbientPressure(*pvalue);
      break;

    case CMND_SCD40_FORCEDRECALIBRATION:
      return scd40.performForcedRecalibration(*pvalue);
      break;

    default:
      // else for Unknown command
    break;
  }
  return 0;  // Fix GCC 10.1 warning
}

/*********************************************************************************************\
 * Command Sensor42
\*********************************************************************************************/

bool Scd40CommandSensor()
{
  char command[CMDSZ];
  bool serviced = true;
  uint8_t prefix_len = strlen(D_CMND_SCD40);
  int error;

  if (!strncasecmp_P(XdrvMailbox.topic, PSTR(D_CMND_SCD40), prefix_len)) {  // prefix
    int command_code = GetCommandCode(command, sizeof(command), XdrvMailbox.topic + prefix_len, kSCD40_Commands);

// not supported here: readMeasurement, getDataReadyStatus, getSerialNumber, measure_single_shot, measure_single_shot_rht_only

    switch (command_code) {
      case CMND_SCD40_ALTITUDE:
      case CMND_SCD40_AUTOMODE:
      case CMND_SCD40_TEMPOFFSET:
      case CMND_SCD40_PRESSURE:            // write-only
      case CMND_SCD40_FORCEDRECALIBRATION: // write-only
      case CMND_SCD40_SELFTEST:            // read-only
      {
        uint16_t value = 0;
        if (XdrvMailbox.data_len > 0)
        {
          if (command_code != CMND_SCD40_SELFTEST) {
            value = XdrvMailbox.payload;
            error = Scd40SetCommand(command_code, &value);
            Response_P(S_JSON_SCD40_COMMAND_NVALUE, command, error?-1:0);
          } else {
            serviced = false;
            break;
          }
        }
        else
        {
          if ((command_code != CMND_SCD40_PRESSURE) && (command_code != CMND_SCD40_FORCEDRECALIBRATION)) {
            error = Scd40GetCommand(command_code, &value);
            Response_P(S_JSON_SCD40_COMMAND_NVALUE, command, error?-1:value);
          } else {
            serviced = false;
            break;
          }
        }
      }
      break;

      case CMND_SCD40_START_MEASUREMENT_LOW_POWER: 
      {
        error = scd40.startLowPowerPeriodicMeasurement(); 
        Response_P(S_JSON_SCD40_COMMAND_NVALUE, command, error?-1:0);
      }
      break;

      case CMND_SCD40_START_MEASUREMENT:           
      {
        error = scd40.startPeriodicMeasurement();          
        Response_P(S_JSON_SCD40_COMMAND_NVALUE, command, error?-1:0);
      }
      break;

      case CMND_SCD40_STOP_MEASUREMENT:            
      {
        error = scd40.stopPeriodicMeasurement();          
        Response_P(S_JSON_SCD40_COMMAND_NVALUE, command, error?-1:0);
      }
      break;

      case CMND_SCD40_PERSIST:                     
      {
        error = scd40.persistSettings();                  
        Response_P(S_JSON_SCD40_COMMAND_NVALUE, command, error?-1:0);
      }
      break;

      case CMND_SCD40_REINIT:                      
      {
        error = scd40.reinit();                           
        Response_P(S_JSON_SCD40_COMMAND_NVALUE, command, error?-1:0);
      }
      break;

      case CMND_SCD40_FACTORYRESET:                
      {
        error = scd40.performFactoryReset();              
        Response_P(S_JSON_SCD40_COMMAND_NVALUE, command, error?-1:0);
      }
      break;

      case CMND_SCD40_SINGLESHOT:                
      {
        error = scd40.measureSingleShot();
        Response_P(S_JSON_SCD40_COMMAND_NVALUE, command, error?-1:0);
      }
      break;

      case CMND_SCD40_SINGLESHOT_RHT_ONLY:                
      {
        error = scd40.measureSingleShotRhtOnly();
        Response_P(S_JSON_SCD40_COMMAND_NVALUE, command, error?-1:0);
      }
      break;

      default:
        // else for Unknown command
        serviced = false;
      break;
    }
  }
  return serviced;
}

void Scd40Show(bool json)
{
  if (scd40IsDataValid)
  {
    float t = ConvertTemp(scd40_Temp);
    float h = ConvertHumidity(scd40_Humid);

    if (json) {
      ResponseAppend_P(PSTR(",\"SCD40\":{\"" D_JSON_CO2 "\":%d,\"" D_JSON_ECO2 "\":%d,"), scd40_CO2, scd40_CO2EAvg);
      ResponseAppendTHD(t, h);
      ResponseJsonEnd();
#ifdef USE_DOMOTICZ
      if (0 == TasmotaGlobal.tele_period) {
        DomoticzSensor(DZ_AIRQUALITY, scd40_CO2);
        DomoticzTempHumPressureSensor(t, h);
      }
#endif  // USE_DOMOTICZ
#ifdef USE_WEBSERVER
    } else {
      WSContentSend_PD(HTTP_SNS_CO2EAVG, "SCD40", scd40_CO2EAvg);
      WSContentSend_PD(HTTP_SNS_CO2, "SCD40", scd40_CO2);
      WSContentSend_THD("SCD40", t, h);
#endif  // USE_WEBSERVER
    }
  }
}

/*********************************************************************************************\
 * Interface
\*********************************************************************************************/

bool Xsns92(byte function)
{
  if (!I2cEnabled(XI2C_62)) { return false; }

  bool result = false;

  if (FUNC_INIT == function) {
    Scd40Detect();
  }
  else if (scd40Found) {
    switch (function) {
      case FUNC_EVERY_SECOND:
        Scd40Update();
        break;
      case FUNC_COMMAND:
        result = Scd40CommandSensor();
        break;
      case FUNC_JSON_APPEND:
        Scd40Show(1);
        break;
#ifdef USE_WEBSERVER
      case FUNC_WEB_SENSOR:
        Scd40Show(0);
        break;
#endif  // USE_WEBSERVER
    }
  }
  return result;
}

#endif  // USE_SCD40
#endif  // USE_I2C
