

#include <DHTesp.h>
#include <HampelFilter.h>

#define DEFAULT_MAX_UNSENT_TEMP_SEC (30*60)

class Temp {

public:
  int gpio;
  int no_of_temp_readings;
  float mad_scaling_factor;
  float temp_correction;
  float humidity_correction;
  float dummy_temp_humidity_val;
  String detector_type;
  String strRawInfoTopic;
  String strMqttRawSuffix;
  unsigned int readings_index;

  // Don't check too often - no need.  This is defaulted in setup to be at more than the minimum dht interval
  // -1 is signal to the defaulting code it still needs setting
  int check_temp_frequency_sec;

  int (*mqttPublishFunc)(const String&, const String&) ;
  
  DHTesp* dht;
  float avg_h;
  float avg_t;
  float median_h;
  float median_t;

  float last_avg_temp_sent;
  float min_reportable_temp_change;

  // 30 minutes maximum space between temp updates, even if temp hasn't or has hardly changed
  int max_unsent_temp_sec = DEFAULT_MAX_UNSENT_TEMP_SEC;
  int temp_unsent_loops;

  HampelFilter* temperatures;
  HampelFilter* humidities;
  
  String strTempTopic;

public:
  Temp(const char* p_detector_type, int (*fp)(const String&, const String&)) {
    detector_type = p_detector_type;
    
    no_of_temp_readings = 250;  // tests with post-process-temperatures.pl suggest these two are best
    mad_scaling_factor = 2.0;
    temp_correction = 0.0f;
    humidity_correction = 0.0f;

    gpio = 0;
    readings_index = 0;

    dummy_temp_humidity_val = -1.0f;
    
    dht = new DHTesp();

    avg_h = 0.0f;
    avg_t = 0.0f;
    median_h = 0.0f;
    median_t = 0.0f;

    last_avg_temp_sent = -999.0f;
    min_reportable_temp_change = 0.1f;

    // 30 minutes maximum space between temp updates, even if temp hasn't or has hardly changed
    max_unsent_temp_sec = DEFAULT_MAX_UNSENT_TEMP_SEC;
    
    mqttPublishFunc = fp;
  }

  String getDetectorType() {
    return detector_type;
  }

  void readConfig(const DynamicJsonDocument& json, const String& strInfoTopic) {

    if (json.containsKey("mqtt_raw_topic_suffix"))
      strMqttRawSuffix = String(json["mqtt_raw_topic_suffix"]);

    if (json.containsKey("dht_gpio"))
      gpio = json["dht_gpio"];

    if (json.containsKey("mad_scaling_factor"))
      mad_scaling_factor = json["mad_scaling_factor"];
      
    if (json.containsKey("temp_correction"))
      temp_correction = json["temp_correction"];
      
    if (json.containsKey("humidity_correction"))
      humidity_correction = json["humidity_correction"];

    if (json.containsKey("temp_outlier"))
      temp_correction = json["temp_outlier"];
      
    if (json.containsKey("humidity_outlier"))
      humidity_correction = json["humidity_outlier"];

    if (json.containsKey("no_of_temp_readings"))
      no_of_temp_readings = json["no_of_temp_readings"];

    if (json.containsKey("max_unsent_temp_sec"))
      max_unsent_temp_sec = json["max_unsent_temp_sec"];

    if (json.containsKey("min_reportable_temp_change"))
      min_reportable_temp_change = json["min_reportable_temp_change"];

    if (json.containsKey("dummy_temp_humidity_val"))
      dummy_temp_humidity_val = json["dummy_temp_humidity_val"];

    // Related temp check frequency and 
    {  
      // No of main loops between temp checks. Looping once a second.  Set to the minimum plus one clear second, rounded up
      check_temp_frequency_sec = (dht->getMinimumSamplingPeriod() + 1500)/1000;

      // If this isn't set, main set-up has to determine it, but don't default here.  Only allow it to be increased.
      if (json.containsKey("check_temp_frequency_sec") && json["check_temp_frequency_sec"] > check_temp_frequency_sec)
        check_temp_frequency_sec = json["check_temp_frequency_sec"];
        
      init_max_unsent_temp_loop_count();
    }
      
    temperatures = new HampelFilter(0.0, no_of_temp_readings, mad_scaling_factor);
    humidities = new HampelFilter(0.0, no_of_temp_readings, mad_scaling_factor);

    dht->setup(gpio, detector_type == "dht11" ? DHTesp::DHT11 : DHTesp::DHT22);
    pinMode(gpio,INPUT);
    
    strTempTopic = strInfoTopic + "-temp";
    
    // Only send frequent, raw messages if configured
    if (!strMqttRawSuffix.isEmpty())
      strRawInfoTopic = strTempTopic + strMqttRawSuffix;
      
    Serial.println(String("Set up DHT: ") + detector_type
         + ", " + check_temp_frequency_sec 
         + " sec reading frequency, min reportable temp change: " + min_reportable_temp_change );
  }
  
  void init_max_unsent_temp_loop_count() {
    temp_unsent_loops = max_unsent_temp_sec/check_temp_frequency_sec/no_of_temp_readings;
  }
  
  void readTempAndHumidity() {

    float h,t;
    DynamicJsonDocument json(1024);
    String strJson;
    
    // this is to fake temp and humidity if you're running without dht board in a usb socket, to test
    if (dummy_temp_humidity_val > 0.0f)
      t = h = dummy_temp_humidity_val;
    else {
      h = dht->getHumidity();
      t = dht->getTemperature();
    }
  
    Serial.println(String(dht->getStatusString()) 
                  + ",\t temp: " + t  + ",\t humidity: " + h + ",\theat index: "
                  + dht->computeHeatIndex(t, h, false));

    if (isnan(t) || isnan(h) || h < 0 || h > 100 || t < -5.0 || t > 40.0 ) {
      Serial.println(String("Failed to read temp/humidity from DHT sensor!, ") + t + ", " + h);
  
      // start again, assuming it starts working again
      readings_index = 0;
      init_max_unsent_temp_loop_count();
  
      if (!strRawInfoTopic.isEmpty()) {
        json["humidity"] = dht->getStatusString();
        json["temperature"] = "error";
        serializeJson(json, strJson);
        (*mqttPublishFunc)(strJson, strRawInfoTopic);
      }
    }
    else {
      temperatures->write(t);
      humidities->write(h);

      // round the floats and reduce to one decimal point, so the mqtt messages aren't too cluttered.
      char buff[10];

      if (!strRawInfoTopic.isEmpty()) {
        json["humidity"] = dtostrf(h, 3, 1, buff);
        json["temperature"] = dtostrf(t, 3, 1, buff);

        serializeJson(json, strJson);
        (*mqttPublishFunc)(strJson, strRawInfoTopic);
        json.clear();
      }
      
      if (++readings_index >= no_of_temp_readings) {
    
        readings_index = 0;
        
        getAverage(temperatures,&avg_t,&median_t);
        avg_t += temp_correction;
        median_t += temp_correction;
    
        getAverage(humidities,&avg_h,&median_h);
        avg_h += humidity_correction;
        median_h += humidity_correction;
    
        // has the temperature changed enough, or long enough gone by for another message?
        if (abs(last_avg_temp_sent - avg_t) > min_reportable_temp_change || temp_unsent_loops <= 0) {
    
          init_max_unsent_temp_loop_count();
    
          last_avg_temp_sent = avg_t;
          
          // Are there any readings yet?
          if (avg_h) {
            json["humidity"] = dtostrf(avg_h, 3, 1, buff);
            json["medianh"] = dtostrf(median_h, 3, 1, buff);
            json["temperature"] = dtostrf(avg_t, 3, 1, buff);
            json["mediant"] = dtostrf(median_t, 3, 1, buff);
          }
        
          json["hum-correct"] = humidity_correction;
          json["temp-correct"] = temp_correction;
          
          serializeJson(json, strJson);
          (*mqttPublishFunc)(strJson, strTempTopic);
        }
        else
          temp_unsent_loops--;
      }
    }
  }

private:
  void getAverage(HampelFilter* h, float* avg, float* median) {
  
    *avg = 0.0f;
    float values[no_of_temp_readings];
    int value_count = 0;
  
    for (int i = 0; i < no_of_temp_readings; i++) {
  
      float val = h->readOrderedValue(i);
  
      if (h->checkIfOutlier(val))
        Serial.println(String("Drop outlier: ") + val);
      else {
        *avg += val;
        values[value_count++] = val;
      }
    }
  
    if (value_count) {
      *avg /= value_count;
  
      // median is the middle value in the ordered list (with MAD exclusions,) if the list length is odd.
      *median = values[value_count/2];
      
      // If it's even it's the average of the middle 2 values.
      if ( !(value_count % 2) ) {
        *median += values[value_count/2 - 1];
        *median /= 2;
      }
    }
    else {
      Serial.println("Nothing but outliers, return median");
      *avg = *median = h->readMedian();  // not much alternative if everything is an outlier
    }
  }
};
