/*

THERMOSTAT MODULE

Copyright (C) 2017 by Dmitry Blinov <dblinov76 at gmail dot com>

*/

#if THERMOSTAT_SUPPORT

#include "relay.h"

#include <ArduinoJson.h>
#include <float.h>

bool _thermostat_enabled = true;
bool _thermostat_mode_cooler = false;

const char* NAME_THERMOSTAT_ENABLED     = "thermostatEnabled";
const char* NAME_THERMOSTAT_MODE        = "thermostatMode";
const char* NAME_TEMP_RANGE_MIN         = "tempRangeMin";
const char* NAME_TEMP_RANGE_MAX         = "tempRangeMax";
const char* NAME_REMOTE_SENSOR_NAME     = "remoteSensorName";
const char* NAME_REMOTE_TEMP_MAX_WAIT   = "remoteTempMaxWait";
const char* NAME_ALONE_ON_TIME          = "aloneOnTime";
const char* NAME_ALONE_OFF_TIME         = "aloneOffTime";
const char* NAME_MAX_ON_TIME            = "maxOnTime";
const char* NAME_MIN_OFF_TIME           = "minOffTime";
const char* NAME_BURN_TOTAL             = "burnTotal";
const char* NAME_BURN_TODAY             = "burnToday";
const char* NAME_BURN_YESTERDAY         = "burnYesterday";
const char* NAME_BURN_THIS_MONTH        = "burnThisMonth";
const char* NAME_BURN_PREV_MONTH        = "burnPrevMonth";
const char* NAME_BURN_DAY               = "burnDay";
const char* NAME_BURN_MONTH             = "burnMonth";
const char* NAME_OPERATION_MODE         = "thermostatOperationMode";

#define ASK_TEMP_RANGE_INTERVAL_INITIAL      15000  // ask initially once per every 15 seconds
#define ASK_TEMP_RANGE_INTERVAL_REGULAR      60000  // ask every minute to be sure
#define MILLIS_IN_SEC                         1000
#define MILLIS_IN_MIN                        60000
#define THERMOSTAT_STATE_UPDATE_INTERVAL     60000 // 1 min
#define THERMOSTAT_RELAY                         0 // use relay 0
#define THERMOSTAT_TEMP_RANGE_MIN               10 // grad. Celsius
#define THERMOSTAT_TEMP_RANGE_MIN_MIN            3 // grad. Celsius
#define THERMOSTAT_TEMP_RANGE_MIN_MAX           30 // grad. Celsius
#define THERMOSTAT_TEMP_RANGE_MAX               20 // grad. Celsius
#define THERMOSTAT_TEMP_RANGE_MAX_MIN            8 // grad. Celsius
#define THERMOSTAT_TEMP_RANGE_MAX_MAX           35 // grad. Celsius
#define THERMOSTAT_ALONE_ON_TIME                 5 //  5 min
#define THERMOSTAT_ALONE_OFF_TIME               55 // 55 min
#define THERMOSTAT_MAX_ON_TIME                  30 // 30 min
#define THERMOSTAT_MIN_OFF_TIME                 10 // 10 min

unsigned long _thermostat_remote_temp_max_wait  = THERMOSTAT_REMOTE_TEMP_MAX_WAIT * MILLIS_IN_SEC;
unsigned long _thermostat_alone_on_time   = THERMOSTAT_ALONE_ON_TIME  * MILLIS_IN_MIN;
unsigned long _thermostat_alone_off_time  = THERMOSTAT_ALONE_OFF_TIME * MILLIS_IN_MIN;
unsigned long _thermostat_max_on_time     = THERMOSTAT_MAX_ON_TIME    * MILLIS_IN_MIN;
unsigned long _thermostat_min_off_time    = THERMOSTAT_MIN_OFF_TIME   * MILLIS_IN_MIN;
unsigned int  _thermostat_on_time_for_day = 0;
unsigned int  _thermostat_burn_total      = 0;
unsigned int  _thermostat_burn_today      = 0;
unsigned int  _thermostat_burn_yesterday  = 0;
unsigned int  _thermostat_burn_this_month = 0;
unsigned int  _thermostat_burn_prev_month = 0;
unsigned int  _thermostat_burn_day        = 0;
unsigned int  _thermostat_burn_month      = 0;

struct temp_t {
  float temp;
  unsigned long last_update = 0;
  bool need_display_update = false;
};
temp_t _remote_temp;

struct temp_range_t {
  int min = THERMOSTAT_TEMP_RANGE_MIN;
  int max = THERMOSTAT_TEMP_RANGE_MAX;
  unsigned long last_update = 0;
  unsigned long ask_time = 0;
  unsigned int  ask_interval = 0;
  bool need_display_update = true;
};
temp_range_t _temp_range;

enum temperature_source_t {temp_none, temp_local, temp_remote};
struct thermostat_t {
  unsigned long last_update = 0;
  unsigned long last_switch = 0;
  String remote_sensor_name;
  unsigned int temperature_source = temp_none;
};
thermostat_t _thermostat;

enum thermostat_cycle_type {cooling, heating};
unsigned int _thermostat_cycle = heating;
String thermostat_remote_sensor_topic;

//------------------------------------------------------------------------------
void thermostatEnabled(bool enabled) {
    _thermostat_enabled = enabled;
}

//------------------------------------------------------------------------------
bool thermostatEnabled() {
    return _thermostat_enabled;
}

//------------------------------------------------------------------------------
void thermostatModeCooler(bool cooler) {
    _thermostat_mode_cooler = cooler;
}

//------------------------------------------------------------------------------
bool thermostatModeCooler() {
    return _thermostat_mode_cooler;
}

//------------------------------------------------------------------------------
std::vector<thermostat_callback_f> _thermostat_callbacks;

void thermostatRegister(thermostat_callback_f callback) {
    _thermostat_callbacks.push_back(callback);
}

//------------------------------------------------------------------------------
void updateOperationMode() {
  #if WEB_SUPPORT
    String message;
    if (_thermostat.temperature_source == temp_remote) {
      message = "{\"thermostatVisible\": 1, \"thermostatOperationMode\": \"remote temperature\"}";
      updateRemoteTemp(true);
    } else if (_thermostat.temperature_source == temp_local) {
      message = "{\"thermostatVisible\": 1, \"thermostatOperationMode\": \"local temperature\"}";
      updateRemoteTemp(false);
    } else {
      message = "{\"thermostatVisible\": 1, \"thermostatOperationMode\": \"autonomous\"}";
      updateRemoteTemp(false);
    }
    wsSend(message.c_str());
  #endif
}

//------------------------------------------------------------------------------
void updateRemoteTemp(bool remote_temp_actual) {
  #if WEB_SUPPORT
      char tmp_str[16];
      if (remote_temp_actual) {
        dtostrf(_remote_temp.temp, 1, 1, tmp_str);
      } else {
        strcpy(tmp_str, "\"?\"");
      }
      char buffer[128];
      snprintf_P(buffer, sizeof(buffer), PSTR("{\"thermostatVisible\": 1, \"remoteTmp\": %s}"), tmp_str);
      wsSend(buffer);
  #endif
}

//------------------------------------------------------------------------------
// MQTT
//------------------------------------------------------------------------------
void thermostatMQTTCallback(unsigned int type, const char * topic, const char * payload) {

    if (type == MQTT_CONNECT_EVENT) {
      mqttSubscribeRaw(thermostat_remote_sensor_topic.c_str());
      mqttSubscribe(MQTT_TOPIC_HOLD_TEMP);
      _temp_range.ask_interval = ASK_TEMP_RANGE_INTERVAL_INITIAL;
      _temp_range.ask_time = millis();
    }

    if (type == MQTT_MESSAGE_EVENT) {

        // Match topic
        String t = mqttMagnitude((char *) topic);

        if (strcmp(topic, thermostat_remote_sensor_topic.c_str()) != 0
         && !t.equals(MQTT_TOPIC_HOLD_TEMP))
           return;

        // Parse JSON input
        DynamicJsonBuffer jsonBuffer;
        JsonObject& root = jsonBuffer.parseObject(payload);
        if (!root.success()) {
            DEBUG_MSG_P(PSTR("[THERMOSTAT] Error parsing data\n"));
            return;
        }

        // Check rempte sensor temperature
        if (strcmp(topic, thermostat_remote_sensor_topic.c_str()) == 0) {
            if (root.containsKey(magnitudeTopic(MAGNITUDE_TEMPERATURE))) {
                String remote_temp = root[magnitudeTopic(MAGNITUDE_TEMPERATURE)];
                _remote_temp.temp = remote_temp.toFloat();
                _remote_temp.last_update = millis();
                _remote_temp.need_display_update = true;
                DEBUG_MSG_P(PSTR("[THERMOSTAT] Remote sensor temperature: %s\n"), remote_temp.c_str());
                updateRemoteTemp(true);
            }
        }

        // Check temperature range change
        if (t.equals(MQTT_TOPIC_HOLD_TEMP)) {
            if (root.containsKey(MQTT_TOPIC_HOLD_TEMP_MIN)) {
                int t_min = root[MQTT_TOPIC_HOLD_TEMP_MIN];
                int t_max = root[MQTT_TOPIC_HOLD_TEMP_MAX];
                if (t_min < THERMOSTAT_TEMP_RANGE_MIN_MIN || t_min > THERMOSTAT_TEMP_RANGE_MIN_MAX ||
                    t_max < THERMOSTAT_TEMP_RANGE_MAX_MIN || t_max > THERMOSTAT_TEMP_RANGE_MAX_MAX) {
                    DEBUG_MSG_P(PSTR("[THERMOSTAT] Hold temperature range error\n"));
                    return;
                }
                _temp_range.min = root[MQTT_TOPIC_HOLD_TEMP_MIN];
                _temp_range.max = root[MQTT_TOPIC_HOLD_TEMP_MAX];
                setSetting(NAME_TEMP_RANGE_MIN, _temp_range.min);
                setSetting(NAME_TEMP_RANGE_MAX, _temp_range.max);
                saveSettings();
                _temp_range.ask_interval = ASK_TEMP_RANGE_INTERVAL_REGULAR;
                _temp_range.last_update = millis();
                _temp_range.need_display_update = true;

                DEBUG_MSG_P(PSTR("[THERMOSTAT] Hold temperature range: (%d - %d)\n"), _temp_range.min, _temp_range.max);
                // Update websocket clients
                #if WEB_SUPPORT
                    char buffer[100];
                    snprintf_P(buffer, sizeof(buffer), PSTR("{\"thermostatVisible\": 1, \"tempRangeMin\": %d, \"tempRangeMax\": %d}"), _temp_range.min, _temp_range.max);
                    wsSend(buffer);
                #endif
            } else {
                DEBUG_MSG_P(PSTR("[THERMOSTAT] Error temperature range data\n"));
            }
        }
    }
}

#if MQTT_SUPPORT
//------------------------------------------------------------------------------
void thermostatSetupMQTT() {
    mqttRegister(thermostatMQTTCallback);
}
#endif 

//------------------------------------------------------------------------------
void notifyRangeChanged(bool min) {
  DEBUG_MSG_P(PSTR("[THERMOSTAT] notifyRangeChanged %s = %d\n"), min ? "MIN" : "MAX", min ? _temp_range.min : _temp_range.max);
  char tmp_str[6];
  sprintf(tmp_str, "%d", min ? _temp_range.min : _temp_range.max);

  mqttSend(min ? MQTT_TOPIC_NOTIFY_TEMP_RANGE_MIN : MQTT_TOPIC_NOTIFY_TEMP_RANGE_MAX, tmp_str, true);
}

//------------------------------------------------------------------------------
// Setup
//------------------------------------------------------------------------------
void commonSetup() {
  _thermostat_enabled     = getSetting(NAME_THERMOSTAT_ENABLED).toInt() == 1;
  DEBUG_MSG_P(PSTR("[THERMOSTAT] _thermostat_enabled = %d\n"), _thermostat_enabled);

  _thermostat_mode_cooler = getSetting(NAME_THERMOSTAT_MODE).toInt() == 1;
  DEBUG_MSG_P(PSTR("[THERMOSTAT] _thermostat_mode_cooler = %d\n"), _thermostat_mode_cooler);
  
  _temp_range.min         = getSetting(NAME_TEMP_RANGE_MIN, THERMOSTAT_TEMP_RANGE_MIN).toInt();
  _temp_range.max         = getSetting(NAME_TEMP_RANGE_MAX, THERMOSTAT_TEMP_RANGE_MAX).toInt();
  DEBUG_MSG_P(PSTR("[THERMOSTAT] _temp_range.min = %d\n"), _temp_range.min);
  DEBUG_MSG_P(PSTR("[THERMOSTAT] _temp_range.max = %d\n"), _temp_range.max);

  _thermostat.remote_sensor_name = getSetting(NAME_REMOTE_SENSOR_NAME);
  thermostat_remote_sensor_topic = _thermostat.remote_sensor_name + String("/") + String(MQTT_TOPIC_JSON);

  _thermostat_remote_temp_max_wait = getSetting(NAME_REMOTE_TEMP_MAX_WAIT, THERMOSTAT_REMOTE_TEMP_MAX_WAIT).toInt()   * MILLIS_IN_SEC;
  _thermostat_alone_on_time   = getSetting(NAME_ALONE_ON_TIME,  THERMOSTAT_ALONE_ON_TIME).toInt()  * MILLIS_IN_MIN;
  _thermostat_alone_off_time  = getSetting(NAME_ALONE_OFF_TIME, THERMOSTAT_ALONE_OFF_TIME).toInt() * MILLIS_IN_MIN;
  _thermostat_max_on_time     = getSetting(NAME_MAX_ON_TIME,    THERMOSTAT_MAX_ON_TIME).toInt()    * MILLIS_IN_MIN;
  _thermostat_min_off_time    = getSetting(NAME_MIN_OFF_TIME,   THERMOSTAT_MIN_OFF_TIME).toInt()   * MILLIS_IN_MIN;
}

//------------------------------------------------------------------------------
void thermostatConfigure() {
  commonSetup();

  _thermostat.temperature_source = temp_none;
  _thermostat_burn_total      = getSetting(NAME_BURN_TOTAL).toInt();
  _thermostat_burn_today      = getSetting(NAME_BURN_TODAY).toInt();
  _thermostat_burn_yesterday  = getSetting(NAME_BURN_YESTERDAY).toInt();
  _thermostat_burn_this_month = getSetting(NAME_BURN_THIS_MONTH).toInt();
  _thermostat_burn_prev_month = getSetting(NAME_BURN_PREV_MONTH).toInt();
  _thermostat_burn_day        = getSetting(NAME_BURN_DAY).toInt();
  _thermostat_burn_month      = getSetting(NAME_BURN_MONTH).toInt();
}

//------------------------------------------------------------------------------
void _thermostatReload() {
  int prev_temp_range_min = _temp_range.min;
  int prev_temp_range_max = _temp_range.max;

  commonSetup();
 
  if (_temp_range.min != prev_temp_range_min)
    notifyRangeChanged(true);
  if (_temp_range.max != prev_temp_range_max)
    notifyRangeChanged(false);
}

#if WEB_SUPPORT
//------------------------------------------------------------------------------
void _thermostatWebSocketOnConnected(JsonObject& root) {
  root["thermostatEnabled"] = thermostatEnabled();
  root["thermostatMode"] = thermostatModeCooler();
  root["thermostatVisible"] = 1;
  root[NAME_TEMP_RANGE_MIN] = _temp_range.min;
  root[NAME_TEMP_RANGE_MAX] = _temp_range.max;
  root[NAME_REMOTE_SENSOR_NAME] = _thermostat.remote_sensor_name;
  root[NAME_REMOTE_TEMP_MAX_WAIT]    = _thermostat_remote_temp_max_wait / MILLIS_IN_SEC;
  root[NAME_MAX_ON_TIME]     = _thermostat_max_on_time    / MILLIS_IN_MIN;
  root[NAME_MIN_OFF_TIME]    = _thermostat_min_off_time   / MILLIS_IN_MIN;
  root[NAME_ALONE_ON_TIME]   = _thermostat_alone_on_time  / MILLIS_IN_MIN;
  root[NAME_ALONE_OFF_TIME]  = _thermostat_alone_off_time / MILLIS_IN_MIN;
  root[NAME_BURN_TODAY]      = _thermostat_burn_today;
  root[NAME_BURN_YESTERDAY]  = _thermostat_burn_yesterday;
  root[NAME_BURN_THIS_MONTH] = _thermostat_burn_this_month;
  root[NAME_BURN_PREV_MONTH] = _thermostat_burn_prev_month;
  root[NAME_BURN_TOTAL]      = _thermostat_burn_total;
  if (_thermostat.temperature_source == temp_remote) {
    root[NAME_OPERATION_MODE] = "remote temperature";
    root["remoteTmp"]     = _remote_temp.temp;
  } else if (_thermostat.temperature_source == temp_local) {
    root[NAME_OPERATION_MODE] = "local temperature";
    root["remoteTmp"]     = "?";
  } else {
    root[NAME_OPERATION_MODE] = "autonomous";
    root["remoteTmp"]     = "?";
  }
}

//------------------------------------------------------------------------------
bool _thermostatWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
    if (strncmp(key, NAME_THERMOSTAT_ENABLED,   strlen(NAME_THERMOSTAT_ENABLED))   == 0) return true;
    if (strncmp(key, NAME_THERMOSTAT_MODE,      strlen(NAME_THERMOSTAT_MODE))      == 0) return true;
    if (strncmp(key, NAME_TEMP_RANGE_MIN,       strlen(NAME_TEMP_RANGE_MIN))       == 0) return true;
    if (strncmp(key, NAME_TEMP_RANGE_MAX,       strlen(NAME_TEMP_RANGE_MAX))       == 0) return true;
    if (strncmp(key, NAME_REMOTE_SENSOR_NAME,   strlen(NAME_REMOTE_SENSOR_NAME))   == 0) return true;
    if (strncmp(key, NAME_REMOTE_TEMP_MAX_WAIT, strlen(NAME_REMOTE_TEMP_MAX_WAIT)) == 0) return true;
    if (strncmp(key, NAME_MAX_ON_TIME,          strlen(NAME_MAX_ON_TIME))          == 0) return true;
    if (strncmp(key, NAME_MIN_OFF_TIME,         strlen(NAME_MIN_OFF_TIME))         == 0) return true;
    if (strncmp(key, NAME_ALONE_ON_TIME,        strlen(NAME_ALONE_ON_TIME))        == 0) return true;
    if (strncmp(key, NAME_ALONE_OFF_TIME,       strlen(NAME_ALONE_OFF_TIME))       == 0) return true;
    return false;
}

//------------------------------------------------------------------------------
void _thermostatWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) {
    if (strcmp(action, "thermostat_reset_counters") == 0) resetBurnCounters();
}
#endif

//------------------------------------------------------------------------------
void thermostatSetup() {
  thermostatConfigure();

  #if MQTT_SUPPORT
    thermostatSetupMQTT();
  #endif

  // Websockets
  #if WEB_SUPPORT
      wsRegister()
          .onConnected(_thermostatWebSocketOnConnected)
          .onKeyCheck(_thermostatWebSocketOnKeyCheck)
          .onAction(_thermostatWebSocketOnAction);
  #endif

  espurnaRegisterLoop(thermostatLoop);
  espurnaRegisterReload(_thermostatReload);
}

//------------------------------------------------------------------------------
void sendTempRangeRequest() {
  DEBUG_MSG_P(PSTR("[THERMOSTAT] sendTempRangeRequest\n"));
  mqttSend(MQTT_TOPIC_ASK_TEMP_RANGE, "", true);
}

//------------------------------------------------------------------------------
void setThermostatState(bool state) {
  DEBUG_MSG_P(PSTR("[THERMOSTAT] setThermostatState: %s\n"), state ? "ON" : "OFF");
  relayStatus(THERMOSTAT_RELAY, state, mqttForward(), false);
  _thermostat.last_switch = millis();
  // Send thermostat change state event to subscribers
  for (unsigned char i = 0; i < _thermostat_callbacks.size(); i++) {
      (_thermostat_callbacks[i])(state);
  }
}

//------------------------------------------------------------------------------
void debugPrintSwitch(bool state, double temp) {
  char tmp_str[16];
  dtostrf(temp, 1, 1, tmp_str);
  DEBUG_MSG_P(PSTR("[THERMOSTAT] switch %s, temp: %s, min: %d, max: %d, mode: %s, relay: %s, last switch %d\n"),
   state ? "ON" : "OFF", tmp_str, _temp_range.min, _temp_range.max, _thermostat_mode_cooler ? "COOLER" : "HEATER", relayStatus(THERMOSTAT_RELAY) ? "ON" : "OFF", millis() - _thermostat.last_switch);
}

//------------------------------------------------------------------------------
inline bool lastSwitchEarlierThan(unsigned int comparing_time) {
  return millis() - _thermostat.last_switch > comparing_time;
}

//------------------------------------------------------------------------------
inline void switchThermostat(bool state, double temp) {
    debugPrintSwitch(state, temp);
    setThermostatState(state);
}

//------------------------------------------------------------------------------
//----------- Main function that make decision ---------------------------------
//------------------------------------------------------------------------------
void checkTempAndAdjustRelay(double temp) {
  if (_thermostat_mode_cooler == false) { // Main operation mode. Thermostat is HEATER.
    // if thermostat switched ON and t > max - switch it OFF and start cooling
    if (relayStatus(THERMOSTAT_RELAY) && temp > _temp_range.max) {
      _thermostat_cycle = cooling;
      switchThermostat(false, temp);
    // if thermostat switched ON for max time - switch it OFF for rest
    } else if (relayStatus(THERMOSTAT_RELAY) && lastSwitchEarlierThan(_thermostat_max_on_time)) {
      switchThermostat(false, temp);
    // if t < min and thermostat switched OFF for at least minimum time - switch it ON and start
    } else if (!relayStatus(THERMOSTAT_RELAY) && temp < _temp_range.min
        && (_thermostat.last_switch == 0 || lastSwitchEarlierThan(_thermostat_min_off_time))) {
      _thermostat_cycle = heating;
      switchThermostat(true, temp);
    // if heating cycle and thermostat switchaed OFF for more than min time - switch it ON
    // continue heating cycle
    } else if (!relayStatus(THERMOSTAT_RELAY) && _thermostat_cycle == heating
        && lastSwitchEarlierThan(_thermostat_min_off_time)) {
      switchThermostat(true, temp);
    }
  } else { // Thermostat is COOLER. Inverse logic.
    // if thermostat switched ON and t < min - switch it OFF and start heating
    if (relayStatus(THERMOSTAT_RELAY) && temp < _temp_range.min) {
      _thermostat_cycle = heating;
      switchThermostat(false, temp);
    // if thermostat switched ON for max time - switch it OFF for rest
    } else if (relayStatus(THERMOSTAT_RELAY) && lastSwitchEarlierThan(_thermostat_max_on_time)) {
      switchThermostat(false, temp);
    // if t > max and thermostat switched OFF for at least minimum time - switch it ON and start
    } else if (!relayStatus(THERMOSTAT_RELAY) && temp > _temp_range.max
        && (_thermostat.last_switch == 0 || lastSwitchEarlierThan(_thermostat_min_off_time))) {
      _thermostat_cycle = cooling;
      switchThermostat(true, temp);
    // if cooling cycle and thermostat switchaed OFF for more than min time - switch it ON
    // continue cooling cycle
    } else if (!relayStatus(THERMOSTAT_RELAY) && _thermostat_cycle == cooling
        && lastSwitchEarlierThan(_thermostat_min_off_time)) {
      switchThermostat(true, temp);
    }
  }
}

//------------------------------------------------------------------------------
void updateCounters() {
  if (relayStatus(THERMOSTAT_RELAY)) {
    setSetting(NAME_BURN_TOTAL,      ++_thermostat_burn_total);
    setSetting(NAME_BURN_TODAY,      ++_thermostat_burn_today);
    setSetting(NAME_BURN_THIS_MONTH, ++_thermostat_burn_this_month);
  }

  if (ntpSynced()) {
    String value = NTP.getDateStr();
    unsigned int day = value.substring(0, 2).toInt();
    unsigned int month = value.substring(3, 5).toInt();
    if (day != _thermostat_burn_day) {
      _thermostat_burn_yesterday = _thermostat_burn_today;
      _thermostat_burn_today = 0;
      _thermostat_burn_day = day;
      setSetting(NAME_BURN_YESTERDAY, _thermostat_burn_yesterday);
      setSetting(NAME_BURN_TODAY,     _thermostat_burn_today);
      setSetting(NAME_BURN_DAY,       _thermostat_burn_day);
    }
    if (month != _thermostat_burn_month) {
      _thermostat_burn_prev_month = _thermostat_burn_this_month;
      _thermostat_burn_this_month = 0;
      _thermostat_burn_month = month;
      setSetting(NAME_BURN_PREV_MONTH, _thermostat_burn_prev_month);
      setSetting(NAME_BURN_THIS_MONTH, _thermostat_burn_this_month);
      setSetting(NAME_BURN_MONTH,      _thermostat_burn_month);
    }
  }
}

//------------------------------------------------------------------------------
double getLocalTemperature() {
  #if SENSOR_SUPPORT
      for (byte i=0; i<magnitudeCount(); i++) {
          if (magnitudeType(i) == MAGNITUDE_TEMPERATURE) {
              double temp = magnitudeValue(i);
              char tmp_str[16];
              dtostrf(temp, 1, 1, tmp_str);
              DEBUG_MSG_P(PSTR("[THERMOSTAT] getLocalTemperature temp: %s\n"), tmp_str);
              return temp > -0.1 && temp < 0.1 ? DBL_MIN : temp;
          }
      }
  #endif
  return DBL_MIN;
}

//------------------------------------------------------------------------------
double getLocalHumidity() {
  #if SENSOR_SUPPORT
      for (byte i=0; i<magnitudeCount(); i++) {
          if (magnitudeType(i) == MAGNITUDE_HUMIDITY) {
              double hum = magnitudeValue(i);
              char tmp_str[16];
              dtostrf(hum, 1, 0, tmp_str);
              DEBUG_MSG_P(PSTR("[THERMOSTAT] getLocalHumidity hum: %s\%\n"), tmp_str);
              return hum > -0.1 && hum < 0.1 ? DBL_MIN : hum;
          }
      }
  #endif
  return DBL_MIN;
}

//------------------------------------------------------------------------------
// Loop
//------------------------------------------------------------------------------
void thermostatLoop(void) {

  if (!thermostatEnabled())
    return;

  // Update temperature range
  if (mqttConnected()) {
    if (millis() - _temp_range.ask_time > _temp_range.ask_interval) {
      _temp_range.ask_time = millis();
      sendTempRangeRequest();
    }
  }

  // Update thermostat state
  if (millis() - _thermostat.last_update > THERMOSTAT_STATE_UPDATE_INTERVAL) {
    _thermostat.last_update = millis();
    updateCounters();
    unsigned int last_temp_src = _thermostat.temperature_source;
    if (_remote_temp.last_update != 0 && millis() - _remote_temp.last_update < _thermostat_remote_temp_max_wait) {
      // we have remote temp
      _thermostat.temperature_source = temp_remote;
      DEBUG_MSG_P(PSTR("[THERMOSTAT] setup thermostat by remote temperature\n"));
      checkTempAndAdjustRelay(_remote_temp.temp);
    } else if (getLocalTemperature() != DBL_MIN) {
      // we have local temp
      _thermostat.temperature_source = temp_local;
      DEBUG_MSG_P(PSTR("[THERMOSTAT] setup thermostat by local temperature\n"));
      checkTempAndAdjustRelay(getLocalTemperature());
      // updateRemoteTemp(false);
    } else {
      // we don't have any temp - switch thermostat on for N minutes every hour
      _thermostat.temperature_source = temp_none;
      DEBUG_MSG_P(PSTR("[THERMOSTAT] setup thermostat by timeout\n"));
      if (relayStatus(THERMOSTAT_RELAY) && millis() - _thermostat.last_switch > _thermostat_alone_on_time) {
        setThermostatState(false);
      } else if (!relayStatus(THERMOSTAT_RELAY) && millis() - _thermostat.last_switch > _thermostat_alone_off_time) {
        setThermostatState(false);
      }
    }
    if (last_temp_src != _thermostat.temperature_source) {
      updateOperationMode();
    }
  }
}

//------------------------------------------------------------------------------
String getBurnTimeStr(unsigned int burn_time) {
  char burnTimeStr[18] = { 0 };
  if (burn_time < 60) {
    sprintf(burnTimeStr, "%d мин.", burn_time);
  } else {
    sprintf(burnTimeStr, "%d ч. %d мин.", (int)floor(burn_time / 60), burn_time % 60);
  }
  return String(burnTimeStr);
}

//------------------------------------------------------------------------------
void resetBurnCounters() {
  DEBUG_MSG_P(PSTR("[THERMOSTAT] resetBurnCounters\n"));
  setSetting(NAME_BURN_TOTAL,      0);
  setSetting(NAME_BURN_TODAY,      0);
  setSetting(NAME_BURN_YESTERDAY,  0);
  setSetting(NAME_BURN_THIS_MONTH, 0);
  setSetting(NAME_BURN_PREV_MONTH, 0);
  _thermostat_burn_total       = 0;
  _thermostat_burn_today       = 0;
  _thermostat_burn_yesterday   = 0;
  _thermostat_burn_this_month  = 0;
  _thermostat_burn_prev_month  = 0;
}

#endif // THERMOSTAT_SUPPORT

//#######################################################################
//  ___   _            _             
// |   \ (_) ___ _ __ | | __ _  _  _ 
// | |) || |(_-<| '_ \| |/ _` || || |
// |___/ |_|/__/| .__/|_|\__,_| \_, |
//              |_|             |__/ 
//#######################################################################

#if THERMOSTAT_DISPLAY_SUPPORT

#include "SSD1306.h" // alias for `#include "SSD1306Wire.h"`

#define wifi_on_width 16
#define wifi_on_height 16
const char wifi_on_bits[] PROGMEM = {
  0x00, 0x00, 0x0E, 0x00, 0x7E, 0x00, 0xFE, 0x01, 0xE0, 0x03, 0x80, 0x07,
  0x02, 0x0F, 0x1E, 0x1E, 0x3E, 0x1C, 0x78, 0x38, 0xE0, 0x38, 0xC0, 0x31,
  0xC6, 0x71, 0x8E, 0x71, 0x8E, 0x73, 0x00, 0x00, };

#define mqtt_width 16
#define mqtt_height 16
const char mqtt_bits[] PROGMEM = {
  0x00, 0x00, 0x00, 0x08, 0x00, 0x18, 0x00, 0x38, 0xEA, 0x7F, 0xEA, 0x7F,
  0x00, 0x38, 0x10, 0x18, 0x18, 0x08, 0x1C, 0x00, 0xFE, 0x57, 0xFE, 0x57,
  0x1C, 0x00, 0x18, 0x00, 0x10, 0x00, 0x00, 0x00, };

#define remote_temp_width 16
#define remote_temp_height 16
const char remote_temp_bits[] PROGMEM = {
  0x00, 0x00, 0xE0, 0x18, 0x10, 0x25, 0x10, 0x25, 0x90, 0x19, 0x50, 0x01,
  0x50, 0x01, 0xD0, 0x01, 0x50, 0x01, 0x50, 0x01, 0xD0, 0x01, 0x50, 0x01,
  0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x00, 0x00, };

#define server_width 16
#define server_height 16
const char server_bits[] PROGMEM = {
  0x00, 0x00, 0xF8, 0x1F, 0xFC, 0x3F, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30,
  0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0xF8, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F,
  0x1E, 0x78, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x00, };

#define LOCAL_TEMP_UPDATE_INTERVAL 60000
#define LOCAL_HUM_UPDATE_INTERVAL  61000

SSD1306  display(0x3c, 1, 3);

unsigned long _local_temp_last_update = 0xFFFF;
unsigned long _local_hum_last_update = 0xFFFF;
bool _display_wifi_status   = true;
bool _display_mqtt_status   = true;
bool _display_server_status = true;
bool _display_remote_temp_status = true;
bool _display_need_refresh  = false;
bool _temp_range_need_update = true;
//------------------------------------------------------------------------------
void drawIco(int16_t x, int16_t y, const char *ico, bool on = true) {
  display.drawIco16x16(x, y, ico, !on);
  _display_need_refresh = true;
}

//------------------------------------------------------------------------------
void display_wifi_status(bool on) {
  _display_wifi_status = on;
  drawIco(0, 0, wifi_on_bits, on);
}

//------------------------------------------------------------------------------
void display_mqtt_status(bool on) {
  _display_mqtt_status = on;
  drawIco(17, 0, mqtt_bits, on);
}

//------------------------------------------------------------------------------
void display_server_status(bool on) {
  _display_server_status = on;
  drawIco(34, 0, server_bits, on);
}

//------------------------------------------------------------------------------
void display_remote_temp_status(bool on) {
  _display_remote_temp_status = on;
  drawIco(51, 0, remote_temp_bits, on);
}

//------------------------------------------------------------------------------
void display_temp_range() {
  _temp_range.need_display_update = false;
  display.setColor(BLACK);
  display.fillRect(68, 0, 60, 16);
  display.setColor(WHITE);
  display.setTextAlignment(TEXT_ALIGN_RIGHT);
  display.setFont(ArialMT_Plain_16);
  String temp_range = String(_temp_range.min) + "°- " + String(_temp_range.max) + "°";
  display.drawString(128, 0, temp_range);
  _display_need_refresh = true;
}

//------------------------------------------------------------------------------
void display_remote_temp() {
  _remote_temp.need_display_update = false;
  display.setColor(BLACK);
  display.fillRect(0, 16, 128, 16);
  display.setColor(WHITE);
  display.setFont(ArialMT_Plain_16);
  display.setTextAlignment(TEXT_ALIGN_LEFT);
  String temp_range_title = String("Remote   t");
  display.drawString(0, 16, temp_range_title);

  String temp_range_vol = String("= ") + (_display_remote_temp_status ? String(_remote_temp.temp, 1) : String("?")) + "°";
  display.drawString(75, 16, temp_range_vol);

  _display_need_refresh = true;
}

//------------------------------------------------------------------------------
void display_local_temp() {
  display.setColor(BLACK);
  display.fillRect(0, 32, 128, 16);
  display.setColor(WHITE);
  display.setFont(ArialMT_Plain_16);
  display.setTextAlignment(TEXT_ALIGN_LEFT);

  String local_temp_title = String("Local    t");
  display.drawString(0, 32, local_temp_title);

  String local_temp_vol = String("= ") + (getLocalTemperature() != DBL_MIN ? String(getLocalTemperature(), 1) : String("?")) + "°";
  display.drawString(75, 32, local_temp_vol);

  _display_need_refresh = true;
}

//------------------------------------------------------------------------------
void display_local_humidity() {
  display.setColor(BLACK);
  display.fillRect(0, 48, 128, 16);
  display.setColor(WHITE);
  display.setFont(ArialMT_Plain_16);
  display.setTextAlignment(TEXT_ALIGN_LEFT);

  String local_hum_title = String("Local    h ");
  display.drawString(0, 48, local_hum_title);

  String local_hum_vol = String("= ") + (getLocalHumidity() != DBL_MIN ? String(getLocalHumidity(), 0) : String("?")) + "%";
  display.drawString(75, 48, local_hum_vol);

  _display_need_refresh = true;
}
//------------------------------------------------------------------------------
// Setup
//------------------------------------------------------------------------------
void displaySetup() {
  display.init();
  display.flipScreenVertically();

  // display.setFont(ArialMT_Plain_24);
  // display.setTextAlignment(TEXT_ALIGN_CENTER);
  // display.drawString(64, 17, "Thermostat");

    espurnaRegisterLoop(displayLoop);
}

//------------------------------------------------------------------------------
void displayLoop() {
  _display_need_refresh = false;

  //------------------------------------------------------------------------------
  // Indicators
  //------------------------------------------------------------------------------
  if (!_display_wifi_status) {
    if (wifiConnected() && WiFi.getMode() != WIFI_AP)
      display_wifi_status(true);
  } else if (!wifiConnected() || WiFi.getMode() == WIFI_AP) {
    display_wifi_status(false);
  }

  if (!_display_mqtt_status) {
    if (mqttConnected())
      display_mqtt_status(true);
  } else if (!mqttConnected()) {
    display_mqtt_status(false);
  }

  if (millis() - _temp_range.last_update < THERMOSTAT_SERVER_LOST_INTERVAL) {
    if (!_display_server_status)
      display_server_status(true);
  } else if (_display_server_status) {
    display_server_status(false);
  }

  if (millis() - _remote_temp.last_update < _thermostat_remote_temp_max_wait) {
    if (!_display_remote_temp_status)
      display_remote_temp_status(true);
  } else if (_display_remote_temp_status) {
    display_remote_temp_status(false);
    display_remote_temp();
  }

  //------------------------------------------------------------------------------
  // Temp range
  //------------------------------------------------------------------------------
  if (_temp_range.need_display_update) {
      display_temp_range();
  }

  //------------------------------------------------------------------------------
  // Remote temp
  //------------------------------------------------------------------------------
  if (_remote_temp.need_display_update) {
      display_remote_temp();
  }

  //------------------------------------------------------------------------------
  // Local temp
  //------------------------------------------------------------------------------
  if (millis() - _local_temp_last_update > LOCAL_TEMP_UPDATE_INTERVAL) {
      _local_temp_last_update = millis();
      display_local_temp();
  }

  //------------------------------------------------------------------------------
  // Local temp
  //------------------------------------------------------------------------------
  if (millis() - _local_hum_last_update > LOCAL_HUM_UPDATE_INTERVAL) {
      _local_hum_last_update = millis();
      display_local_humidity();
  }

  //------------------------------------------------------------------------------
  // Display update
  //------------------------------------------------------------------------------
  if (_display_need_refresh) {
    yield();
    display.display();
  }
}

#endif // THERMOSTAT_DISPLAY_SUPPORT
