/*
  xsns_103_sen5x.ino - SEN5X gas and air quality sensor support for Tasmota

  Copyright (C) 2022  Tyeth Gundry

  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/>.
*/

#ifdef USE_I2C
#ifdef USE_SEN5X
/*********************************************************************************************\
 * SEN5X - Gas (VOC - Volatile Organic Compounds / NOx - Nitrous Oxides) and Particulates (PM)
 *
 * Source: Sensirion SEN5X Driver + Example, and Tasmota Driver 98 by Jean-Pierre Deschamps
 * Adaption for TASMOTA: Tyeth Gundry
 *
 * I2C Address: 0x69
\*********************************************************************************************/

#define XSNS_103 103
#define XI2C_76 76 // See I2CDEVICES.md

#define SEN5X_ADDRESS 0x69
#define SEN5X_PASSIVE_MODE_INTERVAL 10

#include <SensirionI2CSen5x.h>

SensirionI2CSen5x sen5x;

struct SEN5XDATA_s {
  float massConcentrationPm1p0;
  float massConcentrationPm2p5;
  float massConcentrationPm4p0;
  float massConcentrationPm10p0;
  float ambientHumidity;
  float ambientTemperature;
  float vocIndex;
  float noxIndex;
} *SEN5XDATA = nullptr;

/********************************************************************************************/

bool Sen5xError(const char* func, int error) {
  bool result = (error != 0);
  if (result) {
    AddLog(LOG_LEVEL_DEBUG, PSTR("S5X: %s error %d"), func, error);
  }
  return result;
}

/********************************************************************************************/

void sen5x_Init(void) {
  PowerOnDelay(60);  // Sensor startup time (Time after power-on until I2C communication can be started)
  for (uint32_t bus = 0; bus < MAX_I2C; bus++) {
    if (!I2cSetDevice(SEN5X_ADDRESS, bus)) { continue; }
    sen5x.begin(I2cGetWire(bus));

    uint8_t firmwareMajor;
    uint8_t firmwareMinor;
    bool firmwareDebug;
    uint8_t hardwareMajor;
    uint8_t hardwareMinor;
    uint8_t protocolMajor;
    uint8_t protocolMinor;
    unsigned char serial_number[32] = { 0 };
    unsigned char product_name[32] = { 0 };

    if (!Settings->flag6.sen5x_passive_mode) {  // SetOption156 - (Sen5x) Run in passive mode when there is another I2C master (e.g. Ikea Vindstyrka), i.e. do not set up Sen5x sensor, higher polling interval
      if (sen5x.deviceReset()) {                        // Performs delay(200) if no error
        if (Sen5xError("Reset", sen5x.deviceReset())) { // See https://github.com/arendst/Tasmota/discussions/24452
          continue;
        }
      }

      if (Sen5xError("Version", sen5x.getVersion(firmwareMajor, firmwareMinor, firmwareDebug,
                                                 hardwareMajor, hardwareMinor, protocolMajor,
                                                 protocolMinor))) {
        continue;
      }

      if (Sen5xError("Serialnumber", sen5x.getSerialNumber(serial_number, sizeof(serial_number)))) {
        continue;
      }

      if (Sen5xError("Productname", sen5x.getProductName(product_name, sizeof(product_name)))) {
        continue;
      }

      delay(1100);                              // Wait 1 second for sensors to start recording + 100ms for reset command
      if (Sen5xError("Measurement", sen5x.startMeasurement())) {
        continue;
      }
    }

    SEN5XDATA = (SEN5XDATA_s *)calloc(1, sizeof(struct SEN5XDATA_s));
    I2cSetActiveFound(SEN5X_ADDRESS, "SEN5X", bus);

    if (!Settings->flag6.sen5x_passive_mode) {  // SetOption156 - (Sen5x) Run in passive mode when there is another I2C master (e.g. Ikea Vindstyrka), i.e. do not set up Sen5x sensor, higher polling interval
      AddLog(LOG_LEVEL_DEBUG, PSTR("S5X: %s serialnumber %s v%d.%d"), product_name, serial_number, firmwareMajor, firmwareMinor);
    }

    return;
  }
}

void SEN5XUpdate(void) {  // Perform every second to ensure proper operation of the baseline compensation algorithm
  uint16_t error;
  char errorMessage[256];
  DEBUG_SENSOR_LOG(PSTR("Running readMeasuredValues for SEN5X..."));

  error = sen5x.readMeasuredValues(
      SEN5XDATA->massConcentrationPm1p0, SEN5XDATA->massConcentrationPm2p5, SEN5XDATA->massConcentrationPm4p0,
      SEN5XDATA->massConcentrationPm10p0, SEN5XDATA->ambientHumidity, SEN5XDATA->ambientTemperature, SEN5XDATA->vocIndex,
      SEN5XDATA->noxIndex);

  if (error) {
    AddLog(LOG_LEVEL_DEBUG, PSTR("S5X: Failed to retrieve readings"));
#ifdef DEBUG_TASMOTA_SENSOR
    DEBUG_SENSOR_LOG(PSTR("Error trying to execute readMeasuredValues():"));
    errorToString(error, errorMessage, 256);
    DEBUG_SENSOR_LOG(errorMessage);
  } else {
    DEBUG_SENSOR_LOG(PSTR("SEN5x readings:-"));
    DEBUG_SENSOR_LOG(PSTR("MassConcentrationPm1p0: %1_f"), &SEN5XDATA->massConcentrationPm1p0);
    DEBUG_SENSOR_LOG(PSTR("MassConcentrationPm2p5: %1_f"), &SEN5XDATA->massConcentrationPm2p5);
    DEBUG_SENSOR_LOG(PSTR("MassConcentrationPm4p0: %1_f"), &SEN5XDATA->massConcentrationPm4p0);
    DEBUG_SENSOR_LOG(PSTR("MassConcentrationPm10p0: %1_f"), &SEN5XDATA->massConcentrationPm10p0);
    if (isnan(SEN5XDATA->ambientHumidity)) {
      DEBUG_SENSOR_LOG(PSTR("AmbientHumidity: n/a"));
    } else {
      DEBUG_SENSOR_LOG(PSTR("AmbientHumidity: %*_f"), 2, &SEN5XDATA->ambientHumidity);
    }

    if (isnan(SEN5XDATA->ambientTemperature)) {
      DEBUG_SENSOR_LOG(PSTR("AmbientTemperature: n/a"));
    } else {
      DEBUG_SENSOR_LOG(PSTR("AmbientTemperature: %*_f"), 2, &SEN5XDATA->ambientTemperature);
    }

    if (isnan(SEN5XDATA->vocIndex)) {
      DEBUG_SENSOR_LOG(PSTR("VocIndex: n/a"));
    } else {
      DEBUG_SENSOR_LOG(PSTR("VocIndex: %*_f"), 0, &SEN5XDATA->vocIndex);
    }

    if (isnan(SEN5XDATA->noxIndex)) {
      DEBUG_SENSOR_LOG(PSTR("NoxIndex: n/a"));
    } else {
      DEBUG_SENSOR_LOG(PSTR("NoxIndex: %*_f"), 0, &SEN5XDATA->noxIndex);
    }
#endif
  }
}

void SEN5XShow(bool json) {
  char types[10];
  strcpy_P(types, PSTR("SEN5X"));

  float temperature = 0;
  float humidity = 0;
  float abs_humidity = 0;
  bool ahum_available = (!isnan(SEN5XDATA->ambientTemperature) && !isnan(SEN5XDATA->ambientHumidity) && (SEN5XDATA->ambientHumidity > 0));
  if (ahum_available) {
    temperature = ConvertTemp(SEN5XDATA->ambientTemperature);
    humidity = ConvertHumidity(SEN5XDATA->ambientHumidity);
    abs_humidity = CalcTempHumToAbsHum(SEN5XDATA->ambientTemperature, SEN5XDATA->ambientHumidity);
  }

  if (json) {
    ResponseAppend_P(PSTR(",\"%s\":{\"PM1\":%1_f,\"PM2.5\":%1_f,\"PM4\":%1_f,\"PM10\":%1_f,"),
      types,
      &SEN5XDATA->massConcentrationPm1p0, &SEN5XDATA->massConcentrationPm2p5, &SEN5XDATA->massConcentrationPm4p0, &SEN5XDATA->massConcentrationPm10p0);
    if (!isnan(SEN5XDATA->noxIndex)) {
      ResponseAppend_P(PSTR("\"NOx\":%0_f,"), &SEN5XDATA->noxIndex);
    }
    if (!isnan(SEN5XDATA->vocIndex)) {
      ResponseAppend_P(PSTR("\"VOC\":%0_f,"), &SEN5XDATA->vocIndex);
    }
    if (ahum_available) {
      ResponseAppendTHD(temperature, humidity);
      ResponseAppend_P(PSTR(",\"" D_JSON_AHUM "\":%4_f"), &abs_humidity);
    }
    ResponseJsonEnd();
#ifdef USE_WEBSERVER
  } else {
    WSContentSend_PD(HTTP_SNS_F_ENVIRONMENTAL_CONCENTRATION, types, "1", &SEN5XDATA->massConcentrationPm1p0);
    WSContentSend_PD(HTTP_SNS_F_ENVIRONMENTAL_CONCENTRATION, types, "2.5", &SEN5XDATA->massConcentrationPm2p5);
    WSContentSend_PD(HTTP_SNS_F_ENVIRONMENTAL_CONCENTRATION, types, "4", &SEN5XDATA->massConcentrationPm4p0);
    WSContentSend_PD(HTTP_SNS_F_ENVIRONMENTAL_CONCENTRATION, types, "10", &SEN5XDATA->massConcentrationPm10p0);
    if (!isnan(SEN5XDATA->noxIndex)) {
      WSContentSend_PD(HTTP_SNS_F_NOX, types, &SEN5XDATA->noxIndex);
    }
    if (!isnan(SEN5XDATA->vocIndex)) {
      WSContentSend_PD(HTTP_SNS_F_VOC, types, &SEN5XDATA->vocIndex);
    }
    if (ahum_available) {
      WSContentSend_THD(types, temperature, humidity);
      WSContentSend_PD(HTTP_SNS_F_ABS_HUM, types, 4, &abs_humidity);
    }
#endif
  }
}

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

bool Xsns103(uint32_t function) {
  if (!I2cEnabled(XI2C_76)) { return false; }

  bool result = false;

  if (FUNC_INIT == function) {
    sen5x_Init();
  }
  else if (SEN5XDATA != nullptr) {
    switch (function) {
    case FUNC_EVERY_SECOND:
      if (Settings->flag6.sen5x_passive_mode) {  // SetOption156 - (Sen5x) Run in passive mode when there is another I2C master (e.g. Ikea Vindstyrka), i.e. do not set up Sen5x sensor, higher polling interval
        if (TasmotaGlobal.uptime % SEN5X_PASSIVE_MODE_INTERVAL == 0) {
          SEN5XUpdate();
        }
      }
      else {
        SEN5XUpdate();
      }
      break;
    case FUNC_JSON_APPEND:
      SEN5XShow(1);
      break;
#ifdef USE_WEBSERVER
    case FUNC_WEB_SENSOR:
      SEN5XShow(0);
      break;
#endif // USE_WEBSERVER
    }
  }
  return result;
}

#endif // USE_SEN5X
#endif // USE_I2C
