// The below sketch and implementation is based on:


/*********************************************************************************
 *  MIT License
 *  
 *  Copyright (c) 2020-2022 Gregg E. Berman
 *  
 *  https://github.com/HomeSpan/HomeSpan
 *  
 *  Permission is hereby granted, free of charge, to any person obtaining a copy
 *  of this software and associated documentation files (the "Software"), to deal
 *  in the Software without restriction, including without limitation the rights
 *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *  copies of the Software, and to permit persons to whom the Software is
 *  furnished to do so, subject to the following conditions:
 *  
 *  The above copyright notice and this permission notice shall be included in all
 *  copies or substantial portions of the Software.
 *  
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 *  SOFTWARE.
 *  
 ********************************************************************************/
 

// libs
#include <Arduino.h>
// first data_structure.h
#include "data_structure.h"
// then devices...
#include "devices_config.h"
// ... then common config
#include "common_config.h"
// ... then passwords
#include "passwords.h"
// ... then else
#include <esp_wifi.h>
#include <Wire.h>
#include "esp_ota_ops.h"
// Firmware update
#include <HTTPClient.h>
#include <Update.h>

// main library
#include "HomeSpan.h"


// FUNCTIONS DECLARATIONS
bool wifi_connected();
void heartbeat();
void led_blink(void *pvParams);
void check_volts(void*z);
void do_restart_esp(const char *message);
void change_mac();
void make_serial_number(char *org_mac, char *new_mac, char *serial_number);
void make_fw_version(const char *fw_numeric, char *fw_with_date_time);

void do_update();
void updateFirmware(uint8_t *data, size_t len);
int update_firmware_prepare();
bool check_new_fw_on_server(char *firmware_path, char *new_firmware_version); 
bool compare_firmwares(char *current_firmware, char *new_firmware);
bool periodic_firmware_check();

void check_ota_partition();


// FUNCTIONS DECLARATIONS END
// bool diagnostic();

int fw_totalLength = 0;
int fw_currentLength = 0;
bool perform_update_firmware=false;
int update_progress=0;
int old_update_progress=0;
bool blink_led_status=false;
bool fw_update = false;
bool fw_update_on_boot_done = false;


struct_message myData;
// aux variables
float temperature_value;
float humidity_value;
float light_value;
uint8_t low_bat           = 1; 
uint8_t bat_value_pct;
char md_version_value[20];
uint8_t md_mcu_model_value= 0;
const char models[10][15]        = {"other", "ESP32", "ESP32S2", "ESP32S3", "ESP32C3"}; // [number of models][length of string] - used only in Serial not on HomeKit
uint8_t md_charging_value = 0;

uint64_t aux_UpdateBattery_interval;
uint64_t influxdb_heartbeat_interval;
uint64_t bridge_heartbeat_interval;
uint64_t fw_check_interval;

//change_mac variables used also in make_fw_version() AND as mqtt clientID so must be global
char mac_org_char[18];
uint8_t mac_org[6];
char mac_new_char[18];
uint8_t mac_new[6];

// for data received:
bool temperature_received = false;
bool humidity_received    = false;
bool light_received       = false;
bool bat_received         = false;
bool charging_received    = false;

// for fist hearbeat()
bool first_influxdb_check_done = false;
bool first_heartbeat      = true;

// don't write to influxdb if value indicates error
bool error_temperature    = true;
bool error_humidity       = true;
bool error_light          = true;
uint8_t influxdb_error_count = 0;
bool influxdb_connection_ok = false;

bool device_timeout = false;      // used to send fault=1 to HomeKit for each sensor if info is not comming for UPDATE_TIMEOUT_S time

// for tasks
BaseType_t xReturned_check_volts;
TaskHandle_t check_volts_handle = NULL;
BaseType_t xReturned_led_blink;
TaskHandle_t led_blink_handle = NULL;

BaseType_t xReturned_check_charging;
TaskHandle_t check_charging_handle = NULL;

// aux for blinking as pointers was segfaulting 
bool blinking = false;

float volts, bat_pct; 
bool check_battery = false;
uint8_t charging_int = 0;          // 0-NC,  1-ON, 2-FULL,   // 3-off ??
const char charging_states[3][5] = {"NC", "ON", "FULL"};  // "OFF"};

char fw_version[20];
char fw_ver_on_server[50];

// include dependent on config
// MAX17048 - battery fuel gauge, I2C
#if (USE_MAX17048 == 1)
  #include <SparkFun_MAX1704x_Fuel_Gauge_Arduino_Library.h>
  SFE_MAX1704X lipo(MAX1704X_MAX17048);
#endif

#if (BOARD_TYPE == 2)
  #error "ESP32-S2 does NOT have enough memory for FW update!!!"
  #error "choose anothe board or remove this error to continue" 
#endif

#if (USE_INFLUXDB == 1)
  /*
  https://github.com/tobiasschuerg/InfluxDB-Client-for-Arduino
  */
  #include <InfluxDbClient.h>
  #include <InfluxDbCloud.h>
  InfluxDBClient client_influxdb(INFLUXDB_URL, INFLUXDB_ORG, INFLUXDB_BUCKET, INFLUXDB_TOKEN, InfluxDbCloud2CACert);

  bool influxdb_connected();

  // Data point
  Point sensorReadings("measurements");   // for sensors
  Point bridge_status("bridge_status");          // for status (uptime, fw_version)
  Point box_status("box_status");          // for status (uptime, fw_version)
  bool wifi_ok = false;
#endif

#if (USE_MQTT == 1)
  #include <WiFi.h>
  #include <PubSubClient.h>
  #include <WiFiClientSecure.h>
  #include <ArduinoJson.h>

  void mqtt_reconnect();
  void mqtt_callback(char* topic, byte* payload, unsigned int length);  
  bool publishMessage(const char* topic, const char* payload , boolean retained);
  
  WiFiClientSecure mqtt_espClient;
  PubSubClient mqtt_client(mqtt_espClient);
  unsigned long lastMsg = 0;
  unsigned long mqtt_reconnect_last_check = 0;
  u_int8_t mqtt_errors = 0;

  #define MAX_MQTT_TOPIC    100 // 500
  #define MAX_MQTT_PAYLOAD  200 //500
  #define JSON_MAX_SIZE     500 // 1024  

  #include "mqtt-func.h"

#endif

//display SPECIFIC
#if (ZH_ST7789_LCD == 1)
  #include <TFT_eSPI.h>
  TFT_eSPI tft = TFT_eSPI();  // Invoke library
  #ifndef ZH_ST7789_LCD_LED_GPIO
    #error "ZH_ST7789_LCD requires ZH_ST7789_LCD_LED_GPIO/unless it has always on LED"
  #endif  
  #ifndef ZH_ST7789_LCD_VCC_GPIO
    #error "ZH_ST7789_LCD requires ZH_ST7789_LCD_VCC_GPIO/unless it is always on Vcc"
  #endif    
  #ifndef ZH_ST7789_DRIVER_TESTING
    #error "wrong display configured - change the line in User_Setup_Select.h file accordingly"
    #error "PLEASE include ZH_ST7789-240-240.h"
  #endif
  #include "tft_functions.h"
#endif

// include dependent on config END


bool periodic_firmware_check()
{
  bool success = false;
  char fw_update_status[100];
  u_int32_t start = millis();


  char clocktime[33];
  struct tm timeinfo;
  getLocalTime(&timeinfo,10);
  strftime(clocktime,sizeof(clocktime),"%c",&timeinfo);

  char current_firmware_file[255];
  snprintf(current_firmware_file,sizeof(current_firmware_file),"%s/bridge%d.bin.version",UPDATE_FIRMWARE_HOST,DEVICE_ID);
  char cur_fw_ver[50];
  snprintf(cur_fw_ver,sizeof(cur_fw_ver),"%s",BRIDGE_FW);

  if (check_new_fw_on_server(current_firmware_file,fw_ver_on_server))
  {
    if (compare_firmwares(cur_fw_ver, fw_ver_on_server)) 
    {
      Serial.printf("[%s]: Firmware on server (%s) is NEWER than current (%s), it took=%dms\n",__func__,fw_ver_on_server,cur_fw_ver,millis()-start);
      Serial.printf("[%s]: Going to update firmware now!\n",__func__);
      do_update();
      // this shall never happen below as there is an restart in do_update()
      return true;
    }
    else 
    {
      WEBLOG("[%s]: FW on server=%s, my FW=%s, it took=%dms\n",__func__,fw_ver_on_server,cur_fw_ver,millis()-start);
      LOG0  ("[%s]: FW on server=%s, my FW=%s, it took=%dms\n",__func__,fw_ver_on_server,cur_fw_ver,millis()-start);
      snprintf(fw_update_status,sizeof(fw_update_status),"FW on server=%s, my FW=%s",fw_ver_on_server,cur_fw_ver);
    }
    success = true;
  } else 
  {
    WEBLOG("[%s]: ERROR: Cannot check firmware on server!\n",__func__);
    LOG0  ("[%s]: ERROR: Cannot check firmware on server!, it took=%dms\n",__func__,millis()-start);
    snprintf(fw_update_status,sizeof(fw_update_status),"ERROR: Cannot check firmware on server!");
    success = false;
  }  
  #if (USE_MQTT == 1)
    if (wifi_connected())
    {
      if (!mqtt_client.connected()) 
      {
        mqtt_reconnect();
      }
      // Serial.println();
      u_int32_t mqtt_start = millis();
      char mqtt_message[MAX_MQTT_PAYLOAD];
      char mqtt_topic[MAX_MQTT_TOPIC];

      // JSON

      // debug data 
      snprintf(mqtt_topic, sizeof(mqtt_topic),"%s/bridge/%s/firmware",CLIENT,HOSTNAME);
      StaticJsonDocument<JSON_MAX_SIZE> payload;
      
      payload["last_check"]     = clocktime;
      payload["fw_current"]     = cur_fw_ver;
      if (success)
      {
        payload["fw_on_server"] = fw_ver_on_server;
      } else 
      {
        payload["fw_on_server"] = "N/A";
      }


      char payload_json[JSON_MAX_SIZE];
      int size_pl = serializeJson(payload, payload_json);

      #ifdef DEBUG
        Serial.println("\n============ DEBUG PAYLOAD ============");
        Serial.println("Size of payload="+String(size_pl)+" bytes");
        Serial.println("Serialised payload_json:");
        Serial.println(payload_json);
        Serial.println("serializeJsonPretty");
        serializeJsonPretty(payload, Serial);
      #endif

      if (publishMessage(mqtt_topic,payload_json,true))
      {
        LOG1("\n[%s]: Writing bridge \"%s\" FW check to MQTT done, it took %dms\n",__func__,HOSTNAME,millis()-mqtt_start);
        #ifdef DEBUG
          WEBLOG("[%s]: Writing bridge \"%s\" FW check to MQTT done, it took %dms\n",__func__,HOSTNAME,millis()-mqtt_start);
        #endif
      } else 
      {
        LOG0("\n[%s]: Writing bridge \"%s\" FW check to MQTT NOT done, it took %dms\n",__func__,HOSTNAME,millis()-mqtt_start);
        #ifdef DEBUG
          WEBLOG("[%s]: Writing bridge \"%s\" FW check to MQTT NOT done, it took %dms\n",__func__,HOSTNAME,millis()-mqtt_start);
        #endif
      }

      
    }
  #endif


  return success;
}

bool compare_firmwares(char *current_firmware, char *new_firmware)         //true if new firmware is newer than current, false if equal or new is older or not comparable
{
  int cur_fw_major_ver, cur_fw_minor_ver, cur_fw_patch_ver;
  int new_fw_major_ver, new_fw_minor_ver, new_fw_patch_ver;

  #ifdef DEBUG
    Serial.printf("[%s]: Current: %s\n",__func__,current_firmware);
    Serial.printf("[%s]: New    : %s\n",__func__,new_firmware);
  #endif

  uint8_t cfw_dots, nfw_dots;
  for (int i=0; i<strlen(current_firmware); i++)
  {
    if (current_firmware[i] == '.') 
    {
      ++cfw_dots;
    }
  }
  for (int i=0; i<strlen(new_firmware); i++)
  {
    if (new_firmware[i] == '.') 
    {
      ++nfw_dots;
    }
  }

  if (cfw_dots != nfw_dots)
  {
    #ifdef DEBUG
      Serial.printf("[%s]: FW versions are in different formats - cannot compare!\n\tCurrent FW=%s\n\t    New FW=%s\n",__func__,current_firmware,new_firmware);
      Serial.printf("[%s]: cfw_dots=%d, nfw_dots=%d\n",__func__,cfw_dots,nfw_dots);
    #endif
    return false;
  }

  int cfw = sscanf(current_firmware, "%d.%d.%d", &cur_fw_major_ver, &cur_fw_minor_ver, &cur_fw_patch_ver);
  int nfw = sscanf(new_firmware, "%d.%d.%d", &new_fw_major_ver, &new_fw_minor_ver, &new_fw_patch_ver);

  #ifdef DEBUG
    Serial.printf("[%s]: cfw: %d\n",__func__,cfw);
    Serial.printf("[%s]: nfw: %d\n",__func__,nfw);
  #endif  

  if (cfw != nfw)
  {
    #ifdef DEBUG
      Serial.printf("[%s]: One of the versions are not numerical: current SW=%s, new SW=%s - CANNOT COMPARE!\n",__func__,current_firmware,new_firmware);
    #endif
    return false;
  }

  #ifdef DEBUG
    if (new_fw_major_ver > cur_fw_major_ver)            // new major >
    {
      Serial.println("new major bigger");
      return true;
    } else                                              // new major <=
    {
      if (new_fw_major_ver < cur_fw_major_ver)          // new major <
      {
        Serial.println("new major smaller");
        return false;
      } else                                            // new major =
      {
        if (new_fw_minor_ver > cur_fw_minor_ver)        // new minor >
        {
          Serial.println("new minor bigger");
          return true;
        } else                                          // new minor <=
        {
          if (new_fw_minor_ver < cur_fw_minor_ver)      // new minor <
          {
            Serial.println("new minor smaller");
            return false;
          } else                                        // new minor =
          {
            if (new_fw_patch_ver > cur_fw_patch_ver)    // new patch >
            {
              Serial.println("new patch bigger");
              return true;
            } else                                      // new patch <=
            {
              if (new_fw_patch_ver < cur_fw_patch_ver)  // new patch <
              {
                Serial.println("new patch smaller");
                return false;
              } else                                    // new patch =
              {
                Serial.println("new patch equal");
                return false;
              }
            }
          }
        }
      }
    }
  #else
    if (new_fw_major_ver > cur_fw_major_ver) return true;
    else if (new_fw_major_ver < cur_fw_major_ver) return false;
    // major are equal:
    else 
    if (new_fw_minor_ver > cur_fw_minor_ver) return true;
    else if (new_fw_minor_ver < cur_fw_minor_ver) return false;
    // minor are ALSO equal
    else 
    if (new_fw_patch_ver > cur_fw_patch_ver) return true;
    else if (new_fw_patch_ver < cur_fw_patch_ver) return false;  
    // patch are ALSO equal
    else return false;
  #endif
}

bool check_new_fw_on_server(char *firmware_path, char *new_firmware_version)  // true if checked, false if cannot check
/*
https://www.ingdemurtas.it/en/electronics/esp32-self-updating-from-server/
*/
{
  #ifdef DEBUG
    unsigned long free_mem = ESP.getFreeHeap();
    Serial.printf("[%s]: Free heap=%u bytes\n",__func__,free_mem);
  #endif

  // free memory before starting HTTPClient client;
  #if (USE_MQTT == 1)
    mqtt_espClient.stop();
  #endif

  HTTPClient client;
  client.begin(firmware_path);
  int resp = client.GET();
  #ifdef DEBUG
    Serial.printf("[%s]: Response: %d\n",__func__,resp);
  #endif
  if(resp == 200)
  {
      // get length of document (is -1 when Server sends no Content-Length header)
      int totalLength = client.getSize();
      int len = totalLength;
      #ifdef DEBUG
        Serial.printf("FW Size: %u\n",totalLength);
      #endif
      // buffer for read
      uint8_t buff[128] = { 0 };
      // get tcp stream
      WiFiClient * stream = client.getStreamPtr();
      // read all data from server
      while(client.connected() && (len > 0 || len == -1)) 
      {
        size_t size = stream->available();
        if(size) {
          // read up to 128 byte
          int c = stream->readBytes(buff, ((size > sizeof(buff)) ? sizeof(buff) : size));
          if(len > 0) {
              len -= c;
          }
        }
        delay(1);
      }
      
      String fw_temp;
      for (int i=0; i<totalLength; i++)
      {
        if (iscntrl((char)buff[i]) == 0)  // remove control characters from the string i.e. \n or \r
        {
          fw_temp += (char)buff[i];
        }
      }
      #ifdef DEBUG
        Serial.printf("[%s]: New SW version on server: %s\n",__func__,fw_temp);
      #endif

      int nbytes = snprintf(NULL,0,"%s",fw_temp) +1;
      snprintf(new_firmware_version, nbytes,"%s",fw_temp);

      // free memory after 
      #ifdef DEBUG
        LOG0("[%s]: free heap=%u bytes BEFORE  client.end()\n",__func__,ESP.getFreeHeap());
      #endif
      client.end();
      #ifdef DEBUG
        LOG0("[%s]: free heap=%u bytes AFTER  client.end\n",__func__,ESP.getFreeHeap());
      #endif

    return true;
  } else 
  {
    #ifdef DEBUG
      Serial.printf("[%s]: NO new SW\n",__func__);
    #endif
    snprintf(new_firmware_version, 1,"%s","");
    
    // free memory after 
    #ifdef DEBUG
      LOG0("[%s]: free heap=%u bytes BEFORE  client.end()\n",__func__,ESP.getFreeHeap());
    #endif
    client.end();
    #ifdef DEBUG
      LOG0("[%s]: free heap=%u bytes AFTER  client.end\n",__func__,ESP.getFreeHeap());
    #endif


    return false;
  }
}

void heartbeat()
{
  // Serial.println();
  // check free memory
  unsigned long free_mem = ESP.getFreeHeap();
  LOG1("[%s]: Free heap=%u bytes\n",__func__,free_mem);
  if (free_mem < MIN_ALLOWED_HEAP_SIZE) do_restart_esp("Free heap too small!");

  int8_t rssi = WiFi.RSSI();

  u_int32_t influx_start = millis();
  char clocktime[33];
  struct tm timeinfo;
  getLocalTime(&timeinfo,10);
  strftime(clocktime,sizeof(clocktime),"%c",&timeinfo);
  char uptime[32];
  int seconds=esp_timer_get_time()/1e6;
  int secs=seconds%60;
  int mins=(seconds/=60)%60;
  int hours=(seconds/=60)%24;
  int days=(seconds/=24);        
  sprintf(uptime,"%d:%02d:%02d:%02d",days,hours,mins,secs);

  #ifdef DEBUG
    Serial.printf("[%s]: Current time: %s\n",__func__,clocktime);
  #endif

  WEBLOG("[%s]: \"%s\"(%s): Up=%s,Mem=%dbytes, FW check=%dm\n",__func__,HOSTNAME,MODEL,uptime,free_mem,FW_CHECK_INTERVAL_M);
  LOG0  ("[%s]: \"%s\"(%s), version=%s, Up=%s, Mem=%dbytes, RSSI=%d, FW check=%dm\n",__func__,HOSTNAME,MODEL,fw_version,uptime,free_mem,rssi,FW_CHECK_INTERVAL_M);

  #if (USE_INFLUXDB == 1)
    if (influxdb_connection_ok)
    {
      u_int32_t influx_start = millis();
      bridge_status.clearFields();
      bridge_status.clearTags();
      // Add tags
      bridge_status.addTag("device", HOSTNAME);

      bridge_status.addField("clocktime", clocktime);
      bridge_status.addField("uptime", uptime);
      bridge_status.addField("version", fw_version);
      bridge_status.addField("rssi", rssi);
      bridge_status.addField("heap", free_mem);
      bridge_status.addField("fw_check_interval_minutes", FW_CHECK_INTERVAL_M);
      bridge_status.addField("heartbeat_s", HEARTBEAT_INTERVAL_S);
      bridge_status.addField("mcu_model", MODEL);


      LOG1("[%s]: Writing bridge \"%s\" data to influxdb: ",__func__,HOSTNAME);
      LOG1(client_influxdb.pointToLineProtocol(bridge_status));
      LOG1("\n");
      // Write point into buffer
      if (client_influxdb.writePoint(bridge_status))
      {
        LOG0("[%s]: Writing bridge \"%s\" data to influxdb DONE, it took %dms\n",__func__,HOSTNAME,millis()-influx_start);
      } else
      {
        influxdb_connection_ok = false;
        LOG0("[%s]: Writing bridge \"%s\" data to influxdb DONE FAILED!\n",__func__,HOSTNAME);
      }
    } else
    {
      LOG0("[%s]: Cannot write bridge \"%s\" data to InfluxDB - initiating connection check...\n",__func__,HOSTNAME);
      influxdb_connected();
    }
  #endif

  #if (USE_MQTT == 1)
    if (wifi_connected())
    {
      // Serial.println();
      u_int32_t mqtt_start = millis();
      char mqtt_message[MAX_MQTT_PAYLOAD];
      char mqtt_topic[MAX_MQTT_TOPIC];

      // // uptime
      // snprintf(mqtt_topic, sizeof(mqtt_topic),"bridge/%s/uptime",HOSTNAME);
      // snprintf(mqtt_message, sizeof(mqtt_message),"%s",uptime);
      // LOG1("[%s]: Topic=%s, message=%s\n",__func__,mqtt_topic,mqtt_message);
      // publishMessage(mqtt_topic,mqtt_message,true);  

      // JSON

      // debug data 
      // snprintf(mqtt_topic, sizeof(mqtt_topic),"bridge/%s/debug",HOSTNAME);
      snprintf(mqtt_topic, sizeof(mqtt_topic),"%s/bridge/%s/debug",CLIENT,HOSTNAME);
      StaticJsonDocument<JSON_MAX_SIZE> payload;
      
      payload["clocktime"]      = clocktime;
      payload["uptime"]         = uptime;
      payload["rssi"]           = rssi; 
      payload["fw_version"]     = fw_version;
      payload["free_mem"]       = free_mem;
      payload["fw_check_min"]   = FW_CHECK_INTERVAL_M;
      payload["heartbeat_s"]    = HEARTBEAT_INTERVAL_S;
      payload["mcu_model"]      = MODEL;


      char payload_json[JSON_MAX_SIZE];
      int size_pl = serializeJson(payload, payload_json);

      #ifdef DEBUG
        Serial.println("\n============ DEBUG PAYLOAD ============");
        Serial.println("Size of payload="+String(size_pl)+" bytes");
        Serial.println("Serialised payload_json:");
        Serial.println(payload_json);
        Serial.println("serializeJsonPretty");
        serializeJsonPretty(payload, Serial);
      #endif

      if (publishMessage(mqtt_topic,payload_json,true))
      {
        LOG1("[%s]: Writing bridge \"%s\" DEBUG data to MQTT done, it took %dms\n",__func__,HOSTNAME,millis()-mqtt_start);
        #ifdef DEBUG
          WEBLOG("[%s]: Writing bridge \"%s\" DEBUG data to MQTT done, it took %dms\n",__func__,HOSTNAME,millis()-mqtt_start);
        #endif
      } else 
      {
        LOG0("[%s]: Writing bridge \"%s\" DEBUG data to MQTT NOT done, it took %dms\n",__func__,HOSTNAME,millis()-mqtt_start);
        #ifdef DEBUG
          WEBLOG("[%s]: Writing bridge \"%s\" DEBUG data to MQTT NOT done, it took %dms\n",__func__,HOSTNAME,millis()-mqtt_start);
        #endif
      }

      
    }
  #endif

  #if (ZH_ST7789_LCD == 1)
    char message[30];
    snprintf(message, sizeof(message),"heartbeat from %s",HOSTNAME);
    lcd_middle_text(message);
  #endif
  // LOG0("[%s]: current partition: %s\n",__func__,esp_ota_get_running_partition()->label);
  // LOG0("[%s]: PERFORMING esp_ota_mark_app_invalid_rollback_and_reboot\n",__func__);
  // esp_ota_mark_app_invalid_rollback_and_reboot();
}

bool wifi_connected()
{
  if (WiFi.status() == WL_CONNECTED) return true; else return false;
}

bool influxdb_connected()
{
  #if (USE_INFLUXDB == 1)
    if (!wifi_connected) return false;
    u_int32_t check_influx_start = millis();
    if (client_influxdb.validateConnection()) 
    {
      if (influxdb_error_count > 0) WEBLOG("[%s]:  Connected to InfluxDB\n",__func__);
      LOG0("[%s]: Connected to InfluxDB\n",__func__);
      LOG1(client_influxdb.getServerUrl());
      influxdb_error_count=0;
      LOG1("[%s]:  Checking connection to influxdb took %dms\n",__func__,millis()-check_influx_start);
      influxdb_connection_ok = true;
      return true;
    } else 
    {
      LOG0("[%s]: InfluxDB connection failed: ",__func__);
      LOG0(client_influxdb.getLastErrorMessage());
      LOG0("\n");
      ++influxdb_error_count;
      LOG0("[%s]: InfluxDB connection ERROR - number of retries to connect to influxdb=%d\n",__func__,influxdb_error_count);
      WEBLOG("[%s]: InfluxDB connection ERROR - number of retries to connect to influxdb=%d\n",__func__,influxdb_error_count);
      if (influxdb_error_count > MAX_INFLUXDB_ERRORS)
      {
        do_restart_esp("Influxdb connection failures thresholds reached");
      }
      LOG1("[%s]: Checking connection to influxdb took %dms\n",__func__,millis()-check_influx_start);
      influxdb_connection_ok = false;
      return false;
    }
  #endif
  return false;
}

// blinking in rtos
void led_blink(void *pvParams) 
{
  // inspecting stack size for tasks
  #ifdef DEBUG_XTASKS
    UBaseType_t uxHighWaterMark;
    uxHighWaterMark = uxTaskGetStackHighWaterMark( NULL );
  #endif

  #if defined (ERROR_RED_LED_GPIO) // and defined (ERROR_RED_LED_GPIO)
    while (1) {
        digitalWrite(ERROR_RED_LED_GPIO,HIGH);
        vTaskDelay(50/portTICK_RATE_MS);
        digitalWrite(ERROR_RED_LED_GPIO,LOW);
        vTaskDelay(50/portTICK_RATE_MS);

        #ifdef DEBUG_XTASKS
          uxHighWaterMark = uxTaskGetStackHighWaterMark( NULL );
          LOG1("[%s]: led_blink uxHighWaterMark=%d\n",__func__,uxHighWaterMark);
        #endif
    }
  #endif
}
// blinking in rtos END

// restart
void do_restart_esp(const char *message)
{
  #if defined (ERROR_RED_LED_GPIO) 
    digitalWrite(ERROR_RED_LED_GPIO,LOW);
  #endif
  #if defined (STATUS_LED_GPIO) 
    digitalWrite(STATUS_LED_GPIO,LOW);
  #endif
  LOG0("\n[%s]: %s\n\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n          RESTARTING ESP!\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n\n",__func__,message);
  ESP.restart();
}

void check_volts(void*z)
{
  unsigned long free_mem;

  // inspecting stack size for tasks
  #ifdef DEBUG_XTASKS
    UBaseType_t uxHighWaterMark;
    uxHighWaterMark = uxTaskGetStackHighWaterMark( NULL );
  #endif

    while(1)
      {
        // check free memory
        free_mem = ESP.getFreeHeap();
        LOG1("[%s]: Free heap=%u bytes\n",__func__,free_mem);
        if (free_mem < MIN_ALLOWED_HEAP_SIZE) do_restart_esp("Free heap too small!");

        // check battery
        #if (USE_MAX17048 == 1)
          volts   = lipo.getVoltage();
          bat_pct = lipo.getSOC();
          if (bat_pct>100) bat_pct=100; // we don't need crazy % here
          if (bat_pct<0)   bat_pct=0;

          LOG2("[%s]: Bridge volts: %0.2f, battery percent=%0.2f%%\n",__func__,volts,bat_pct);
          if (bat_pct < LOW_BATTERY_THRESHOLD)
          {
            #ifdef DEBUG
              Serial.printf("[%s]: Volts too low: %0.2f, battery percent=%0.2f%%\n",__func__,volts,bat_pct);
            #endif
            // blink LEDs in task, if not yet blinking
            if (!blinking)
            {
              xReturned_led_blink = xTaskCreate(led_blink, "led_blink", 1800, NULL, 1, &led_blink_handle);
              if( xReturned_led_blink != pdPASS )
              {
                Serial.printf("[%s]: CANNOT create led_blink task\n",__func__);
              } else 
              {
                #ifdef DEBUG
                  Serial.printf("[%s]: Task led_blink created\n",__func__);
                #endif
                blinking = true;
              }
            } else 
            // blink LEDs already blinking
            {
              #ifdef DEBUG
                Serial.printf("[%s]: Task led_blink ALREADY created\n",__func__);
              #endif
            }
          } else
          {
            #ifdef DEBUG
              Serial.printf("[%s]: Volts OK: %0.2f, battery percent=%0.2f%%\n",__func__,volts,bat_pct);
            #endif
            if (blinking)
            {
              #ifdef DEBUG
                Serial.printf("[%s]: Disabling blinking LEDs\n",__func__);
                Serial.printf("[%s]: DELETING TASK\n",__func__);
              #endif
              vTaskDelete( led_blink_handle );
              blinking = false;
            }
          }
        #endif
        // check battery END

        // check charging
        #if defined(CHARGING_GPIO) and defined(POWER_GPIO)
        /*
          - both GPIO must be PULLUP as LOW is active from TP4056
          - LEDs on TP4056 are NOT needed if PULL_UP both GPIO
          
          #define POWER_GPIO                38  // GREEN, STDB PIN6 on TP4056, LOW on CHARGED (LED ON),       comment out if not in use - don't use "0" here
          #define CHARGING_GPIO             39  // RED,   CHRG PIN7 on TP4056, LOW during CHARGING (LED ON),  comment out if not in use - don't use "0" here
          
          truth table:
          NC:               POWER_GPIO HIGH (PULLUP)    CHARGING_GPIO HIGH (PULLUP)
          CHARGING:         POWER_GPIO HIGH (PULLUP)    CHARGING_GPIO LOW     
          FULL (CHARGED):   POWER_GPIO LOW              CHARGING_GPIO HIGH (PULLUP)
          OFF (DISABLED):   POWER_GPIO HIGH (PULLUP)    CHARGING_GPIO HIGH (PULLUP) ???? - not checked yet

          */

          uint8_t power_gpio_state    = digitalRead(POWER_GPIO);
          uint8_t charging_gpio_state = digitalRead(CHARGING_GPIO);

          if ((power_gpio_state)   and (charging_gpio_state))   charging_int = 0; // NC
          if ((power_gpio_state)   and (!charging_gpio_state))  charging_int = 1; // ON/CHARGING
          if ((!power_gpio_state)  and (charging_gpio_state))   charging_int = 2; // FULL/CHARGED
          // if ((digitalRead(POWER_GPIO) == 1) and (digitalRead(CHARGING_GPIO) == 1)) charging_int = 3; // OFF ??

          LOG2("[%s]: charging=%s  charging_int=%d  CHARGING_GPIO=%d  POWER_GPIO=%d\n",__func__,charging_states[charging_int],charging_int,charging_gpio_state,power_gpio_state);
          #ifdef DEBUG
            Serial.printf("[%s]: charging=%s  charging_int=%d  CHARGING_GPIO=%d  POWER_GPIO=%d\n",__func__,charging_states[charging_int],charging_int,charging_gpio_state,power_gpio_state);
          #endif
        #endif
        // check charging END

        vTaskDelay(pdMS_TO_TICKS(1000));

        #ifdef DEBUG_XTASKS
          uxHighWaterMark = uxTaskGetStackHighWaterMark( NULL );
          LOG1("[%s]: check_volts uxHighWaterMark=%d\n",__func__,uxHighWaterMark);
        #endif
      }
}


// update firmware
// FW upgrade wrapper
void do_update()
{

  // free memory used for MQTT before starting HTTPClient client;
  #ifdef DEBUG
    LOG0("[%s]: free heap=%u bytes BEFORE mqtt_espClient.stop()\n",__func__,ESP.getFreeHeap());
  #endif
  #if (USE_MQTT == 1)
    mqtt_espClient.stop();
  #endif
  #ifdef DEBUG
    LOG0("[%s]: free heap=%u bytes AFTER  mqtt_espClient.stop()\n",__func__,ESP.getFreeHeap());
  #endif

  unsigned long free_mem = ESP.getFreeHeap();
  LOG0("[%s]: free heap=%u bytes\n",__func__,free_mem);
  if (free_mem < 66000) 
  {
    LOG0("[%s]: NOT ENOUGH MEMORY FOR FW UPDATE!!!\n",__func__);
    do_restart_esp("NOT ENOUGH MEMORY");
    return;
  }

  Serial.printf("[%s]: FW UPDATE starting...\n",__func__);
  int update_firmware_status = -1;
  update_firmware_status=update_firmware_prepare();
  if (update_firmware_status == 0)
  {
    Serial.printf("[%s]: RESTARTING - FW update SUCCESSFULL\n\n",__func__);
    // blink slowly when FW upgrade successfull
    for (int i=0;i<3;i++)
    {
      #ifdef ERROR_RED_LED_GPIO
        digitalWrite(ERROR_RED_LED_GPIO,LOW);
        delay(100);
        digitalWrite(ERROR_RED_LED_GPIO,HIGH);
        delay(30);
      #elif defined(STATUS_LED_GPIO)
        digitalWrite(STATUS_LED_GPIO,LOW);
        delay(100);
        digitalWrite(STATUS_LED_GPIO,HIGH);
        delay(30);
        // digitalWrite(ERROR_RED_LED_GPIO,LOW);
      #endif
    }

  } else
  {
    Serial.printf("[%s]: FW update failed - reason: %d\n",__func__,update_firmware_status);
    #ifdef ERROR_RED_LED_GPIO
      // sos(ERROR_RED_LED_GPIO);
    #elif defined(STATUS_LED_GPIO)
      // sos(STATUS_LED_GPIO);
    #endif
  }
  // 
  do_restart_esp("FW update finished");
}

// real update
void updateFirmware(uint8_t *data, size_t len)
{
  // blink ERROR_RED_LED_GPIO or...
  #ifdef ERROR_RED_LED_GPIO
    if (blink_led_status) {
      blink_led_status=LOW;
      digitalWrite(ERROR_RED_LED_GPIO,blink_led_status);
    } else {
      blink_led_status=HIGH;
      digitalWrite(ERROR_RED_LED_GPIO,blink_led_status);
    }
  #else
    // ...blink STATUS_LED_GPIO
    #ifdef STATUS_LED_GPIO
      if (blink_led_status) {
        blink_led_status=LOW;
        digitalWrite(STATUS_LED_GPIO,blink_led_status);
      } else {
        blink_led_status=HIGH;
        digitalWrite(STATUS_LED_GPIO,blink_led_status);
      }
    #endif
  #endif

  Update.write(data, len);
  fw_currentLength += len;
  old_update_progress=update_progress;
  update_progress=(fw_currentLength * 100) / fw_totalLength;
  if (update_progress>old_update_progress){
    if (update_progress % 5 == 0){ //it prints every 5%
      Serial.printf("[%s]: FW update: %d%%\n",__func__,update_progress);
    }
  }
  // if current length of written firmware is not equal to total firmware size, repeat
  if(fw_currentLength != fw_totalLength) return;
  Update.end(true);
  Serial.printf("\n[%s]: Update Success, Total Size: %d bytes\n",__func__,fw_currentLength);
}

// download from webserver
int update_firmware_prepare()
{
  HTTPClient firmware_update_client;

  char firmware_file[255];
  snprintf(firmware_file,sizeof(firmware_file),"%s/bridge%d.bin",UPDATE_FIRMWARE_HOST,DEVICE_ID);

  fw_totalLength=0;
  fw_currentLength=0;
  Serial.printf("[%s]: uploading file: %s\n",__func__,firmware_file);
  firmware_update_client.begin(firmware_file);
  int resp = firmware_update_client.GET();
  Serial.printf("[%s]: Response: %d\n",__func__,resp);
  // If file is reachable, start downloading
  if(resp == 200){
    // get length of document (is -1 when Server sends no Content-Length header)
    fw_totalLength = firmware_update_client.getSize();
    // transfer to local variable
    int len = fw_totalLength;
    // this is required to start firmware update process
    Update.begin(UPDATE_SIZE_UNKNOWN);
    Serial.printf("[%s]: FW Size: %lu bytes\n",__func__,fw_totalLength);

    // create buffer for read
    uint8_t buff[128] = { 0 };
    // get tcp stream
    WiFiClient * stream = firmware_update_client.getStreamPtr();
    // read all data from server
    Serial.printf("[%s]: Updating firmware progress:\n",__func__);
    while(firmware_update_client.connected() && (len > 0 || len == -1))
    {
      // get available data size
      size_t size = stream->available();
      if(size) {
        // read up to 128 byte
        int c = stream->readBytes(buff, ((size > sizeof(buff)) ? sizeof(buff) : size));
        // pass to function
        updateFirmware(buff, c);
        if(len > 0) {
           len -= c;
        }
      }
      delay(1);
      }
  }else
  {
    Serial.printf("[%s]: Cannot download firmware file. Only HTTP response 200: OK is supported. Double check firmware location.\n",__func__);
    Serial.printf("[%s]: firmware update prepare UNSUCESSFUL\n",__func__);
    // free memory after 
    #ifdef DEBUG
      LOG0("[%s]: free heap=%u bytes BEFORE  firmware_update_client.end()\n",__func__,ESP.getFreeHeap());
    #endif
    firmware_update_client.end();
    #ifdef DEBUG
      LOG0("[%s]: free heap=%u bytes AFTER  firmware_update_client.end\n",__func__,ESP.getFreeHeap());
    #endif
    return resp;
  }
  // free memory after 
  #ifdef DEBUG
    LOG0("[%s]: free heap=%u bytes BEFORE  firmware_update_client.end()\n",__func__,ESP.getFreeHeap());
  #endif
  firmware_update_client.end();
  #ifdef DEBUG
    LOG0("[%s]: free heap=%u bytes AFTER  firmware_update_client.end\n",__func__,ESP.getFreeHeap());
  #endif

  Serial.printf("[%s]: firmware update prepare SUCESSFULL\n",__func__);
  return 0;
}
// update firmware END

void change_mac()
{
  WiFi.mode(WIFI_STA);
  WiFi.macAddress(mac_org);

  snprintf(mac_org_char, sizeof(mac_org_char), "%02x:%02x:%02x:%02x:%02x:%02x",mac_org[0], mac_org[1], mac_org[2], mac_org[3], mac_org[4], mac_org[5]);
  Serial.printf("[%s]: OLD MAC: %s\n",__func__,mac_org_char);

  Serial.printf("[%s]: changing MAC...",__func__);
  if (esp_wifi_set_mac(WIFI_IF_STA, &FixedMACAddress[0]) == ESP_OK) LOG0("SUCCESSFULL\n"); else LOG0("FAILED\n");

  WiFi.macAddress(mac_new);
  snprintf(mac_new_char, sizeof(mac_new_char), "%02x:%02x:%02x:%02x:%02x:%02x",mac_new[0], mac_new[1], mac_new[2], mac_new[3], mac_new[4], mac_new[5]);
  Serial.printf("[%s]: NEW MAC: %s\n",__func__,mac_new_char);
}

void make_serial_number(char *org_mac, char*new_mac, char *buff)
{
  String mac_org_str = String(org_mac);
  String mac_new_str = String(new_mac);

  mac_org_str.replace(":","");
  mac_new_str.replace(":",""); 
  mac_new_str.remove(0,6);

  // find the size of the new string formatted
  size_t nbytes = snprintf(NULL,0,"%s_%s",mac_org_str,mac_new_str) +1;

  // final serial_number in format: "oldmac_6charofnewmac"
  snprintf(buff,nbytes,"%s_%s",mac_org_str,mac_new_str);

  #ifdef DEBUG
    Serial.printf("[%s]: serial_number: %s\n",__func__,buff);
  #endif
}

void make_fw_version(const char *fw_numeric, char *fw_with_date_time)
{
  int month, day, year, zh_hour,zh_minute, short_y;
  static const char month_names[] = "JanFebMarAprMayJunJulAugSepOctNovDec";
  // day, year done
  sscanf(__DATE__, "%s %d %d", fw_with_date_time, &day, &year);
  // short year
  short_y=(((year / 10U) % 10)* 10)  + ((year / 1U) % 10);
  //month done
  month = (strstr(month_names, fw_with_date_time)-month_names)/3+1;
  // time
  String time_str = String(__TIME__);
  time_str.replace(":"," ");
  char time_chr[30];
  snprintf(time_chr,sizeof(time_chr),"%s",time_str);
  sscanf(time_chr, "%d %d", &zh_hour, &zh_minute);

  // tmp char
  char fw_char[20];
  // numeric to string
  String fw_str = String(fw_numeric);
  // dots to space
  fw_str.replace("."," ");
  // string to char
  snprintf(fw_char,sizeof(fw_char),"%s",fw_str);
  int major,minor,patch;
  // digits to int
  sscanf(fw_char, "%d %d %d", &major, &minor, &patch);
  snprintf(fw_char,sizeof(fw_char),"%d",major*100+minor*10+patch);

  // final fw_version in format: FW.DATE.TIME (of compilation) where FW= major(hundreds)+minor(tens)+patch(units)
  size_t nbytes =   snprintf(NULL,0,"%s.%02d%02d%02d.%02d%02d",fw_char, short_y, month, day,zh_hour,zh_minute)+1;
  snprintf(fw_with_date_time,nbytes,"%s.%02d%02d%02d.%02d%02d",fw_char, short_y, month, day,zh_hour,zh_minute);

  #ifdef DEBUG
    Serial.printf("[%s]: fw_version: %s\n",__func__,fw_with_date_time);
  #endif
}

// REMOTE DEVICES 
struct UpdateData : Service::AccessoryInformation
{
  SpanCharacteristic *md_version;

  SpanPoint *remoteData;
  const char *name;
  const char *macAddress;
  
  UpdateData(const char *name, const char*macAddress) : Service::AccessoryInformation()
  {
    this->name=name;
    this->macAddress=macAddress;
    // not updatable
    new Characteristic::Identify();
    new Characteristic::Name(name);
    new Characteristic::SerialNumber(macAddress);  
    
    // updatable
    md_version    = new Characteristic::FirmwareRevision("");
    #if (ENCRYPT_ESPNOW == 0)
      SpanPoint::setEncryption(false);
    #else
      SpanPoint::setEncryption(true);
    #endif
    remoteData    = new SpanPoint(macAddress,0,sizeof(myData));    // create a SpanPoint with send size=0 and receive size=sizeof(myData)
  }

  void loop()
  {                                       // if there is data from the remote sensor
    if(remoteData->get(&myData))
    {                                       // if there is data from the remote sensor
      // should RED LED blink when new message comes? maybe not..
      #ifdef BLINK_ERROR_LED_ON_RECEIVED_DATA
        #ifdef ERROR_RED_LED_GPIO
          digitalWrite(ERROR_RED_LED_GPIO,HIGH);
        #endif
      #endif
      // but maybe the Status LED?
      #ifdef BLINK_STATUS_LED_ON_RECEIVED_DATA
        if (digitalRead(homeSpan.getStatusPin())) digitalWrite(homeSpan.getStatusPin(),LOW);
      #endif

      LOG1("\n[%s]: Update from device=%s...\n",__func__,macAddress);
      temperature_value     = myData.md_temp;       // update temperature
      temperature_received  = true;
      humidity_value        = myData.md_hum;        // update humidity
      humidity_received     = true;
      light_value           = myData.md_light;      // update light
      light_received        = true;
      md_mcu_model_value    = myData.md_mcu_model;  // update model
      bat_value_pct         = myData.md_bat;        // update battery percent
      bat_received          = true;
      md_charging_value     = myData.md_charging;   // update charging
      charging_received     = true;

      snprintf(md_version_value,sizeof(md_version_value),"%s",myData.md_version); // update version
      u_int32_t free_m = ESP.getFreeHeap();
      LOG0  ("[%s]: [%s]: temp=%0.2fC, hum=%0.2f%%, light=%0.2flx, bat_value_pct=%d%%, volt=%0.2fV, charging=%d, RSSI=%ddBm, model=%d (%s), version=%s, wifi_ok=%d\n",__func__,name,temperature_value,humidity_value,light_value,bat_value_pct,myData.md_bat_volts,md_charging_value,myData.md_rssi,md_mcu_model_value,models[md_mcu_model_value],md_version_value,myData.wifi_ok);
      WEBLOG("[%s]: T=%0.2f H=%0.2f L=%0.2f Bat=%d%% Volt=%0.2fV CH=%s RSSI=%ddBm Board=%s v=%s, wifi_ok=%d\n",name,temperature_value,humidity_value,light_value,bat_value_pct,myData.md_bat_volts,charging_states[md_charging_value],myData.md_rssi,models[md_mcu_model_value],md_version_value,myData.wifi_ok);

      // Free=%dbytes, free_m

      md_version->setString(md_version_value);                    // update version

      device_timeout = false;      // clean timeout

      // workaround for -99lx as we cannot determine if overloaded or 0:

      float modified_light;
      if (light_value < 0) 
      {
        modified_light = 0.0f;
      } else 
      {
        modified_light = light_value;
      }

      // publish influxdb devices
      #if (USE_INFLUXDB == 1)
        if (influxdb_connection_ok)
          {
            u_int32_t influx_start = millis();
            sensorReadings.clearFields();
            sensorReadings.clearTags();
            sensorReadings.addTag("device", name);
              // Add readings as fields to point, if value is no error
            if(!error_temperature) 
              sensorReadings.addField("temperature", myData.md_temp);
            if(!error_humidity) 
              sensorReadings.addField("humidity", myData.md_hum);
            if (!error_light) 
              // sensorReadings.addField("light", myData.md_light);
              sensorReadings.addField("light", modified_light);

            sensorReadings.addField("battery", (float)myData.md_bat);

            sensorReadings.addField("charging", myData.md_charging);

            sensorReadings.addField("volts", (float)myData.md_bat_volts);

            LOG1("[%s]: Writing box: \"%s\" measurements data to influxdb: ",__func__,name);
            LOG1(client_influxdb.pointToLineProtocol(sensorReadings));
            LOG1("\n");
            
            // Add status data
            box_status.clearFields();
            box_status.clearTags();
            box_status.addTag("device", name);

            box_status.addField("version", md_version_value);
            box_status.addField("model", models[md_mcu_model_value]);
            box_status.addField("chargingstate", charging_states[md_charging_value]);

            box_status.addField("boot_reason", myData.md_boot_reason);
            box_status.addField("wakeup_rason", myData.md_wakeup_reason);
            box_status.addField("bootcount", myData.md_bootcount);
            box_status.addField("rssi", myData.md_rssi);
            box_status.addField("wifi_ok", myData.wifi_ok);

            LOG1("[%s]: Writing box: \"%s\" status data to influxdb: ",__func__,name);
            LOG1(client_influxdb.pointToLineProtocol(box_status));
            LOG1("\n");



            // Write point into buffer
            if ((client_influxdb.writePoint(sensorReadings)) and (client_influxdb.writePoint(box_status)))
            {
              LOG0("[%s]: Writing box \"%s\" data to influxdb DONE, it took %dms\n",__func__,name,millis()-influx_start);
            } else
            {
              influxdb_connection_ok = false;
              LOG0("[%s]: Writing box \"%s\" data to influxdb DONE FAILED!\n",__func__,name);
            }            
          } else
          {
            LOG0("[%s]: Cannot write box \"%s\" data to InfluxDB - initiating connection check...\n",__func__,name);
            influxdb_connected();
          }
      #endif
      // publish influxdb devices END



      #if (USE_MQTT == 1)
        if (wifi_connected())
        {
          // Serial.println();
          u_int32_t mqtt_start = millis();
          char mqtt_message[MAX_MQTT_PAYLOAD];
          char mqtt_topic[MAX_MQTT_TOPIC];

          // JSON

          // debug data 
          snprintf(mqtt_topic, sizeof(mqtt_topic),"%s/box/%s/debug",CLIENT,name);
          StaticJsonDocument<JSON_MAX_SIZE> payload;
          
          payload["fw_version"]     = md_version_value;
          payload["mcu_model"]      = models[md_mcu_model_value]; 
          payload["charging"]       = charging_states[md_charging_value];
          payload["boot_reason"]    = myData.md_boot_reason;
          payload["wakeup_rason"]   = myData.md_wakeup_reason;
          payload["bootcount"]      = myData.md_bootcount;
          payload["rssi"]           = myData.md_rssi;
          payload["wifi_ok"]        = myData.wifi_ok;

          char payload_json[JSON_MAX_SIZE];
          int size_pl = serializeJson(payload, payload_json);

          #ifdef DEBUG
            Serial.println("\n============ DEBUG PAYLOAD ============");
            Serial.println("Size of payload="+String(size_pl)+" bytes");
            Serial.println("Serialised payload_json:");
            Serial.println(payload_json);
            Serial.println("serializeJsonPretty");
            serializeJsonPretty(payload, Serial);
          #endif

          
          if (publishMessage(mqtt_topic,payload_json,true))
          {
            LOG1("\n[%s]: Writing box \"%s\" DEBUG data to MQTT done, it took %dms\n",__func__,HOSTNAME,millis()-mqtt_start);
            #ifdef DEBUG
              WEBLOG("[%s]: Writing box \"%s\" DEBUG data to MQTT done, it took %dms\n",__func__,HOSTNAME,millis()-mqtt_start);
            #endif
          } else 
          {
            LOG0("\n[%s]: Writing box \"%s\" DEBUG data to MQTT NOT done, it took %dms\n",__func__,HOSTNAME,millis()-mqtt_start);
            #ifdef DEBUG
              WEBLOG("[%s]: Writing box \"%s\" DEBUG data to MQTT NOT done, it took %dms\n",__func__,HOSTNAME,millis()-mqtt_start);
            #endif
          }

          // box data 
          snprintf(mqtt_topic, sizeof(mqtt_topic),"%s/box/%s/sensors",CLIENT,name);
          
          char temp_char[10];
          snprintf(temp_char, sizeof(temp_char),"%0.2f",myData.md_temp);

          char hum_char[10];
          snprintf(hum_char, sizeof(hum_char),"%0.2f",myData.md_hum);

          char hum_light[10];
          snprintf(hum_light, sizeof(hum_light),"%0.2f",modified_light);

          char volts_char[10];
          snprintf(volts_char, sizeof(volts_char),"%0.2f",myData.md_bat_volts);

          payload.clear();
          payload["temperature"]  = temp_char;
          payload["humidity"]     = hum_char; 
          payload["light"]        = hum_light;
          payload["battery"]      = myData.md_bat;
          payload["charging"]     = myData.md_charging;
          payload["volts"]        = volts_char;

          size_pl = serializeJson(payload, payload_json);

          #ifdef DEBUG
            Serial.println("\n============ DEBUG PAYLOAD ============");
            Serial.println("Size of payload="+String(size_pl)+" bytes");
            Serial.println("Serialised payload_json:");
            Serial.println(payload_json);
            Serial.println("serializeJsonPretty");
            serializeJsonPretty(payload, Serial);
          #endif

          if (publishMessage(mqtt_topic,payload_json,true))
          {
            LOG1("[%s]: Writing box \"%s\" SENSORS data to MQTT done, it took %dms\n",__func__,HOSTNAME,millis()-mqtt_start);
            #ifdef DEBUG
              WEBLOG("[%s]: Writing box \"%s\" SENSORS data to MQTT done, it took %dms\n",__func__,HOSTNAME,millis()-mqtt_start);
            #endif
          } else 
          {
            LOG0("[%s]: Writing box \"%s\" SENSORS data to MQTT NOT done, it took %dms\n",__func__,HOSTNAME,millis()-mqtt_start);
            #ifdef DEBUG
              WEBLOG("[%s]: Writing box \"%s\" SENSORS data to MQTT NOT done, it took %dms\n",__func__,HOSTNAME,millis()-mqtt_start);
            #endif
          }
        }
      #endif

      #if (ZH_ST7789_LCD == 1)
        char message[30];
        snprintf(message, sizeof(message),"%s data arrived",name);
        lcd_middle_text(message);
      #endif


    } else 
    // DON'T PRINT HERE ANYTHING!
    if (remoteData->time()>(UPDATE_TIMEOUT_S * 1000)) 
    { 
      device_timeout = true;
    }
  }
};

struct RemoteTempSensor : Service::TemperatureSensor
{
  SpanCharacteristic *temp;
  SpanCharacteristic *fault;
  
  const char *name;
  const char *macAddress;

  RemoteTempSensor(const char *name, const char*macAddress) : Service::TemperatureSensor()
  {
    this->name=name;
    this->macAddress=macAddress;
    temp=new Characteristic::CurrentTemperature(0);           // set initial temperature
    temp->setRange(-50,150);                                  // expand temperature range to allow negative values
    fault=new Characteristic::StatusFault(0);                 // set initial state = no fault
  }

  void loop()
  {
    if (temperature_received)
    {
      if (temperature_value < -90)
      {
        fault->setVal(1);                                     // set fault state = FAULT
        LOG0("[%s]: ERROR: temperature from sensor \"%s\" on device \"%s\" INVALID=%f\n",__func__,name,macAddress,temperature_value);
        temperature_received = false;
        error_temperature = true;
      } else
      {
        fault->setVal(0);                                     // set fault state = OK                      
        temp->setVal(temperature_value);              
        LOG1("[%s]: Temperature from sensor \"%s\" on device %s=%f, fault=%d\n",__func__,name,macAddress,temperature_value,fault->getVal());
        temperature_received = false;
        error_temperature = false;
      }
    } else 
    // DON'T PRINT HERE ANYTHING!
    if (device_timeout) 
    {
      fault->setVal(1);   
    }
  }
};

struct RemoteHumSensor : Service::HumiditySensor
{
  SpanCharacteristic *hum;
  SpanCharacteristic *fault;
  const char *name;
  const char *macAddress;

  RemoteHumSensor(const char *name, const char*macAddress) : Service::HumiditySensor()
  {
    this->name=name;
    this->macAddress=macAddress;
    hum=new Characteristic::CurrentRelativeHumidity(0);     // set initial humidity
    hum->setRange(0,100);                                   // expand hum range
    fault=new Characteristic::StatusFault(0);               // set initial state = no fault
  }

  void loop()
  {
    if (humidity_received)
    {
      if (humidity_value < 0)
      {
        fault->setVal(1);                                     // set fault state = FAULT
        LOG0("[%s]: ERROR: Humidity from sensor \"%s\" on device \"%s\" INVALID=%f\n",__func__,name,macAddress,humidity_value);
        humidity_received = false;
        error_humidity = true;
      } else
      {
        fault->setVal(0);                                     // set fault state = OK
        hum->setVal(humidity_value);              // update humidity
        LOG1("[%s]: Humidity from sensor %s on device%s=%f\n",__func__,name,macAddress,humidity_value);
        humidity_received = false;
        error_humidity = false;
      }
    } else
    // DON'T PRINT HERE ANYTHING!
    if (device_timeout) 
    {
      fault->setVal(1);   
    }
  }
};

struct RemoteLightSensor : Service::LightSensor
{
  SpanCharacteristic *lux;
  SpanCharacteristic *fault;
  const char *name;
  const char *macAddress;

  RemoteLightSensor(const char *name, const char*macAddress) : Service::LightSensor()
  {
    this->name=name;
    this->macAddress=macAddress;
    lux=new Characteristic::CurrentAmbientLightLevel(0.1);    // set initial light
    fault=new Characteristic::StatusFault(0);                 // set initial state = fault
  }

  void loop()
  {
    if (light_received)
    {
      if (light_value < 0)
      {
        fault->setVal(1);                                     // set fault state = FAULT
        LOG0("[%s]: ERROR: Light from sensor \"%s\" on device \"%s\" INVALID=%f\n",__func__,name,macAddress,light_value);
        light_received = false;
        error_light = true;
      } else
      {
        fault->setVal(0);                                     // set fault state = OK
        lux->setVal(light_value);                             // update light
        LOG1("[%s]: Light from sensor %s on device %s=%f\n",__func__,name,macAddress,light_value);
        light_received = false;
        error_light = false;
      }
    } else 
    // DON'T PRINT HERE ANYTHING!
    if (device_timeout) 
    {
      fault->setVal(1);   
    }
  }

};

struct RemoteBattery : Service::BatteryService
{
  SpanCharacteristic *battery_level;
  SpanCharacteristic *charging_state;
  SpanCharacteristic *low_battery;
  const char *name;
  const char *macAddress;

  RemoteBattery(const char *name, const char*macAddress) : Service::BatteryService()
  {
    this->name=name;
    this->macAddress=macAddress;
    new Characteristic::Name("Battery sensor");
    battery_level     = new Characteristic::BatteryLevel(0);                     // set initial state to 0%
    charging_state    = new Characteristic::ChargingState(0);                    // set initial state to "not charging" 
    low_battery       = new Characteristic::StatusLowBattery(1);                 // set initial state to true
  }

  void loop()
  {
    if (bat_received)
    {
      battery_level->setVal(bat_value_pct);   
      if (bat_value_pct < 30) 
      {
        low_bat=1;
      } else
      {
        low_bat=0;
      }
      low_battery->setVal(low_bat);  
      bat_received = false;

      if (charging_received)
      {
        if (md_charging_value > 0)
          charging_state->setVal(1);
        else 
          charging_state->setVal(0);
      }
      LOG1("[%s]: Battery data from sensor %s on device %s: battery level=%d%%, low_battery=%d, charging=%d\n",__func__,name,macAddress,bat_value_pct,low_bat,md_charging_value);
      
      charging_received = false;

      // should RED LED blink when new message comes? maybe not..
      #ifdef BLINK_ERROR_LED_ON_RECEIVED_DATA
        #ifdef ERROR_RED_LED_GPIO
          digitalWrite(ERROR_RED_LED_GPIO,LOW);
        #endif
      #endif
      // but maybe the Status LED?
      #ifdef BLINK_STATUS_LED_ON_RECEIVED_DATA
        digitalWrite(homeSpan.getStatusPin(),HIGH);
      #endif
    } else 
    // DON'T PRINT HERE ANYTHING!
    if (device_timeout) 
    {
      device_timeout = false; // clear timeout as all data already done for this device and this timeout is not relevant to next devices
    }
  }
};
// REMOTE DEVICES END


// BRIDGE
struct UpdateBattery : Service::BatteryService
{
  SpanCharacteristic *battery_level;
  SpanCharacteristic *charging_state;
  SpanCharacteristic *low_battery;

  UpdateBattery() : Service::BatteryService()
  {
    new Characteristic::Name("Battery sensor");
    battery_level     = new Characteristic::BatteryLevel(50);                    // set initial state to 50%
    charging_state    = new Characteristic::ChargingState(0);                    // set initial state to "not charging"
    low_battery       = new Characteristic::StatusLowBattery(0);                 // set initial state to false (no alarm)
  }

  void loop()
  {
    if (millis() >= aux_UpdateBattery_interval + (BATTERY_INTERVAL_S * 1000))
    {
      #if defined(CHARGING_GPIO) and defined(POWER_GPIO)
        LOG1("[%s]: Bridge charging status=%d(%s)\n",__func__,charging_int,charging_states[charging_int]);
        if ((charging_int == 1) or (charging_int == 2))
        {
          charging_state->setVal(1);
        }
        else 
        {
          charging_state->setVal(0);
        }
      // #else // don't enable - it floods the screen
      //   LOG0("[%s]: Checking charging DISABLED!\n",__func__);
      #endif 

      #if (USE_MAX17048 == 1)
        if (check_battery)
        {
          battery_level->setVal(bat_pct);  
          LOG1("[%s]: Bridge battery status: volts=%0.2fV, percent=%0.2f%%\n",__func__,volts,bat_pct);
          if (bat_pct < LOW_BATTERY_THRESHOLD)
          {
            low_battery->setVal(1);  
            LOG0("[%s]: Bridge battery CRITICAL status: volts=%0.2fV, percent=%0.2f%%, charging=%d(%s)\n",__func__,volts,bat_pct,charging_int,charging_states[charging_int]);
          } else 
          {
            low_battery->setVal(0);  
          }
          // publish influxdb bridge battery
          #if (USE_INFLUXDB == 1)
            if (influxdb_connection_ok)
            {
              u_int32_t influx_start = millis();
              sensorReadings.clearFields();
              sensorReadings.clearTags();
              sensorReadings.addTag("device", HOSTNAME);
              sensorReadings.addField("battery", (float)bat_pct);
              sensorReadings.addField("charging", charging_states[charging_int]);
              LOG1("[%s]: Writing bridge: \"%s\" status data to influxdb: ",__func__,HOSTNAME);
              LOG1(client_influxdb.pointToLineProtocol(sensorReadings));
              LOG1("\n");

              if (client_influxdb.writePoint(sensorReadings))
              {
                LOG0("[%s]: Writing \"%s\" data to influxdb DONE, it took %dms\n",__func__,HOSTNAME,millis()-influx_start);
              } else
              {
                influxdb_connection_ok = false;
                LOG0("[%s]: Writing \"%s\" data to influxdb DONE FAILED!\n",__func__,HOSTNAME);
              }  

              u_int32_t free_m = ESP.getFreeHeap();
              WEBLOG("Free=%dbytes, %s: Bat=%0.2f%% CH=%s v=%s\n",free_m,HOSTNAME,bat_pct,charging_states[charging_int],fw_version);
            } else
            {
              LOG0("[%s]: Cannot write \"%s\" data to InfluxDB - initiating connection check...\n",__func__,HOSTNAME);
              influxdb_connected();
            }
          #endif
          // publish influxdb bridge battery END

          #if (USE_MQTT == 1)
            if (wifi_connected())
            {
              // Serial.println();
              u_int32_t mqtt_start = millis();
              char mqtt_message[MAX_MQTT_PAYLOAD];
              char mqtt_topic[MAX_MQTT_TOPIC];

              // JSON

              // debug data 
              snprintf(mqtt_topic, sizeof(mqtt_topic),"%s/bridge/%s/sensors",CLIENT,HOSTNAME);
              StaticJsonDocument<JSON_MAX_SIZE> payload;
              
              payload["battery"] = bat_pct;
              payload["charging"] = charging_states[charging_int];


              char payload_json[JSON_MAX_SIZE];
              int size_pl = serializeJson(payload, payload_json);

              #ifdef DEBUG
                Serial.println("\n============ DEBUG PAYLOAD ============");
                Serial.println("Size of payload="+String(size_pl)+" bytes");
                Serial.println("Serialised payload_json:");
                Serial.println(payload_json);
                Serial.println("serializeJsonPretty");
                serializeJsonPretty(payload, Serial);
              #endif

              if (publishMessage(mqtt_topic,payload_json,true))
              {
                LOG1("\n[%s]: Writing bridge \"%s\" SENSORS data to MQTT done, it took %dms\n",__func__,HOSTNAME,millis()-mqtt_start);
                #ifdef DEBUG
                  WEBLOG("[%s]: Writing bridge \"%s\" SENSORS data to MQTT done, it took %dms\n",__func__,HOSTNAME,millis()-mqtt_start);
                #endif
              } else 
              {
                LOG0("\n[%s]: Writing bridge \"%s\" SENSORS data to MQTT NOT done, it took %dms\n",__func__,HOSTNAME,millis()-mqtt_start);
                #ifdef DEBUG
                  WEBLOG("[%s]: Writing bridge \"%s\" SENSORS data to MQTT NOT done, it took %dms\n",__func__,HOSTNAME,millis()-mqtt_start);
                #endif
              }

              
            }
          #endif


        } else 
        {
          low_battery->setVal(0);  
        }
      #endif
      aux_UpdateBattery_interval = millis();
    }
  }
};

struct DEV_Identify : Service::AccessoryInformation 
{
  int nBlinks;                    // number of times to blink built-in LED in identify routine
  SpanCharacteristic *identify;   // reference to the Identify Characteristic
  
  DEV_Identify(const char *name, const char *manu, const char *sn, const char *model, const char *version, int nBlinks) : Service::AccessoryInformation()
  {
    
    new Characteristic::Name(name);                   // create all the required Characteristics with values set based on above arguments
    new Characteristic::Manufacturer(manu);
    new Characteristic::SerialNumber(sn);    
    new Characteristic::Model(model);
    new Characteristic::FirmwareRevision(version);
    identify=new Characteristic::Identify();          // store a reference to the Identify Characteristic for use below

    this->nBlinks=nBlinks;                            // store the number of times to blink the LED
  }

  boolean update()
  { 
    #ifdef ERROR_RED_LED_GPIO    
        LOG0("[%s]: bridge called to identify itself, blinking RED LED...\n",__func__);
        for(int i=0;i<nBlinks;i++)
        {
            digitalWrite(ERROR_RED_LED_GPIO,HIGH);
            delay(50);
            digitalWrite(ERROR_RED_LED_GPIO,LOW);
            delay(50);
        }
    #elif defined(STATUS_LED_GPIO)
        LOG0("[%s]: bridge called to identify, blinking status LED...\n",__func__);
        for(int i=0;i<nBlinks;i++)
        {
            digitalWrite(STATUS_LED_GPIO,HIGH);
            delay(50);
            digitalWrite(STATUS_LED_GPIO,LOW);
            delay(50);
        }
    #else 
        LOG1("[%s]: no LED defined - failed\n",__func__);
        return false;
    #endif
    LOG0("[%s]: bridge identified\n",__func__);


    do_update();
    return true;
  }

};
// BRIDGE END

void setup() 
{

  #if ARDUINO_USB_CDC_ON_BOOT == 1  // delay for S3 mini or others using native USB
    delay(5000);
  #endif

  Serial.begin(115200);
  delay(50);
  Serial.printf("\n======= S T A R T =======\n");

  // apply new MAC address
  change_mac(); // it provides: mac_org_char,  mac_new_char changed globally

  // make new SN char based on old and new MACs
  char serial_number[20];
  make_serial_number(mac_org_char,mac_new_char,serial_number);

  make_fw_version(BRIDGE_FW, fw_version);

  // display init
  #if (ZH_ST7789_LCD == 1)
    #ifdef ZH_ST7789_LCD_VCC_GPIO
      pinMode(ZH_ST7789_LCD_VCC_GPIO,OUTPUT);
      digitalWrite(ZH_ST7789_LCD_VCC_GPIO,1);
    #endif
    #ifdef ZH_ST7789_LCD_LED_GPIO
      pinMode(ZH_ST7789_LCD_LED_GPIO,OUTPUT);
      digitalWrite(ZH_ST7789_LCD_LED_GPIO,1);
    #endif    
    tft.init();
    tft.setRotation(SCREEN_ROTATION);
    tft.fillScreen(TFT_BLACK);
    // tft_template();
    lcd_middle_text("LCD DONE"); 
  #endif
  // display init END


  // intro
  LOG0("[%s]: HOSTNAME: %s\n",__func__,HOSTNAME);
  LOG0("[%s]: FW VERSION: %s\n",__func__,BRIDGE_FW);
  LOG0("[%s]: COMPILATION TIME: %s %s\n",__func__,__DATE__,__TIME__);
  LOG0("[%s]: BOARD TYPE: %s\n",__func__,MODEL);
  LOG0("[%s]: Free heap=%u bytes\n",__func__,ESP.getFreeHeap());

  LOG0("[%s]: BRIDGE_FW on HomeKit: %s\n",__func__,fw_version);
  LOG0("[%s]: SERIAL NUMBER: %s\n",__func__,serial_number);

  LOG0("[%s]: HEARTBEAT_INTERVAL_S: %d\n",__func__,HEARTBEAT_INTERVAL_S);
  LOG0("[%s]: FW_CHECK_INTERVAL_M: %d\n",__func__,FW_CHECK_INTERVAL_M);
  LOG0("[%s]: ENCRYPT_ESPNOW: %d\n",__func__,ENCRYPT_ESPNOW);

  #ifdef ERROR_RED_LED_GPIO
    pinMode(ERROR_RED_LED_GPIO,OUTPUT);
  #endif

  #if (USE_INFLUXDB == 1)
    // Enable messages batching and retry buffer
    client_influxdb.setWriteOptions(WriteOptions().writePrecision(WRITE_PRECISION).batchSize(MAX_BATCH_SIZE).bufferSize(WRITE_BUFFER_SIZE));
    client_influxdb.setInsecure();
  #endif

  homeSpan.setApSSID(HOSTNAME);
  homeSpan.setApPassword("");   // no password for CP
  // homeSpan.enableOTA();         // homespan-ota
  #if (START_CP_ON_BOOT == 1)
    homeSpan.enableAutoStartAP(); // AP on startup if no WiFi credentials
  #endif

  homeSpan.setHostNameSuffix("");;
  // homeSpan.setStatusAutoOff(5);  // turn OFF LED - not good as no info about the life of bridge then 

  // start checking charging
  #if defined(CHARGING_GPIO) and defined(POWER_GPIO)
    #ifdef DEBUG
      Serial.printf("[%s]: CHARGING_GPIO and POWER_GPIO enabled\n",__func__);
    #endif
    pinMode(CHARGING_GPIO, INPUT_PULLUP);   // must be PULLUP as TP4056 is Active LOW
    pinMode(POWER_GPIO, INPUT_PULLUP);      // must be PULLUP as TP4056 is Active LOW
  #else
    #ifdef DEBUG
      Serial.printf("[%s]: checking CHARGING DISABLED\n",__func__);
    #endif
  #endif
  // start start checking charging END  

  #if defined(CUSTOM_SDA_GPIO) and defined(CUSTOM_SCL_GPIO)
    #ifdef DEBUG
        Serial.printf("[%s]: CUSTOM I2C GPIO pins applied: SDA=%d, SCL=%d\n",__func__,CUSTOM_SDA_GPIO,CUSTOM_SCL_GPIO);
    #endif
      Wire.setPins(CUSTOM_SDA_GPIO,CUSTOM_SCL_GPIO);
  #endif
  Wire.begin();
  delay(100); 

  // MAX17048 - fuel gauge
  #if (USE_MAX17048 == 1)
    unsigned long max17048_begin_time = millis();
    #ifdef DEBUG
      lipo.enableDebugging();
      Serial.printf("[%s]: start USE_MAX17048\n",__func__);
    #endif

    if (lipo.begin())
    {
      // lipo.quickStart();     // not needed rather, MAX17048 can recalculate in the time
      check_battery = true;
      lipo.disableHibernate();
      #ifdef DEBUG
        Serial.printf("[%s]: start MAX17048 OK\n",__func__);
      #endif
      xReturned_check_volts = xTaskCreate(check_volts, "check_volts", 2500, NULL, 1, &check_volts_handle);
      if( xReturned_check_volts != pdPASS )
      {
        Serial.printf("[%s]: check_volts task not created !!!ś\n",__func__);
      } else
      {
        Serial.printf("[%s]: check_volts task created\n",__func__);
      } 
    } else 
    {
      Serial.printf("[%s]: MAX17048 NOT detected ... Check your wiring or I2C ADDR!\n",__func__);
      check_battery = false;
    } 

  #else
    #ifdef DEBUG
      Serial.printf("[%s]: DONT USE_MAX17048\n",__func__);
    #endif
  #endif  
  // MAX17048 - fuel gauge END

  homeSpan.setLogLevel(LOG_LEVEL);
  homeSpan.setControlPin(CONTROL_PIN);
  homeSpan.setStatusPin(STATUS_LED_GPIO);
  #if (USE_WEBLOG == 1)
    homeSpan.enableWebLog(MAX_WEBLOG_ENTRIES,NTP_SERVER,"UTC","stats");   // opens webpage with logs
    LOG0("[%s]: WEBLOG enabled\n",__func__);
  #else
    LOG0("[%s]: WEBLOG DISABLED\n",__func__);
  #endif
  homeSpan.setTimeServerTimeout(NTP_SERVER_TIMEOUT_S);
  homeSpan.setSketchVersion(fw_version);
  

// BRIDGE:
  new SpanAccessory();
    new DEV_Identify(HOSTNAME,MANUFACTURER,serial_number,MODEL,fw_version,IDENTIFY_BLINKS);
    new UpdateBattery();

// add accessories only for DEVICE_ID = 1 as of now - don't mess while testing other boards - ok, add also to device 2
  #if ((DEVICE_ID == 1) or (DEVICE_ID == 2) or (DEVICE_ID == 3))
  // DEVICE 1
    new SpanAccessory();
      new UpdateData(DEVICE_ID_1_NAME,DEVICE_ID_1);                 
      new RemoteTempSensor("Temperature Sensor",DEVICE_ID_1);        
      new RemoteHumSensor("Humidity Sensor",DEVICE_ID_1);        
      new RemoteLightSensor("Light Sensor",DEVICE_ID_1);  
      new RemoteBattery(DEVICE_ID_1_NAME,DEVICE_ID_1);    

  // DEVICE 2
    new SpanAccessory();
      new UpdateData(DEVICE_ID_2_NAME,DEVICE_ID_2);                 
      new RemoteTempSensor("Temperature Sensor",DEVICE_ID_2);        
      new RemoteHumSensor("Humidity Sensor",DEVICE_ID_2);        
      new RemoteLightSensor("Light Sensor",DEVICE_ID_2);       
      new RemoteBattery(DEVICE_ID_2_NAME,DEVICE_ID_2);     

  // DEVICE 3
    new SpanAccessory();
      new UpdateData(DEVICE_ID_3_NAME,DEVICE_ID_3);                 
      new RemoteTempSensor("Temperature Sensor",DEVICE_ID_3);        
      new RemoteHumSensor("Humidity Sensor",DEVICE_ID_3);        
      new RemoteLightSensor("Light Sensor",DEVICE_ID_3);  
      new RemoteBattery(DEVICE_ID_3_NAME,DEVICE_ID_3);    


  // DEVICE 4
    new SpanAccessory();
      new UpdateData(DEVICE_ID_4_NAME,DEVICE_ID_4);                 
      new RemoteTempSensor("Temperature Sensor",DEVICE_ID_4);        
      new RemoteHumSensor("Humidity Sensor",DEVICE_ID_4);        
      // new RemoteLightSensor("Light Sensor",DEVICE_ID_4);  
      new RemoteBattery(DEVICE_ID_4_NAME,DEVICE_ID_4);    


  // DEVICE 5
    new SpanAccessory();
      new UpdateData(DEVICE_ID_5_NAME,DEVICE_ID_5);                 
      new RemoteTempSensor("Temperature Sensor",DEVICE_ID_5);        
      new RemoteHumSensor("Humidity Sensor",DEVICE_ID_5);        
      new RemoteLightSensor("Light Sensor",DEVICE_ID_5);  
      new RemoteBattery(DEVICE_ID_5_NAME,DEVICE_ID_5); 

  // DEVICE 6
    new SpanAccessory();
      new UpdateData(DEVICE_ID_6_NAME,DEVICE_ID_6);                 
      new RemoteTempSensor("Temperature Sensor",DEVICE_ID_6);        
      new RemoteHumSensor("Humidity Sensor",DEVICE_ID_6);        
      new RemoteLightSensor("Light Sensor",DEVICE_ID_6);  
      new RemoteBattery(DEVICE_ID_6_NAME,DEVICE_ID_6); 

  // DEVICE 7
    new SpanAccessory();
      new UpdateData(DEVICE_ID_7_NAME,DEVICE_ID_7);                 
      new RemoteTempSensor("Temperature Sensor",DEVICE_ID_7);        
      new RemoteHumSensor("Humidity Sensor",DEVICE_ID_7);        
      new RemoteLightSensor("Light Sensor",DEVICE_ID_7);  
      new RemoteBattery(DEVICE_ID_7_NAME,DEVICE_ID_7); 



// ************************************************************************************************************************************************************
// test bridge and test devices:
  #elif ((DEVICE_ID == 11) or (DEVICE_ID == 12))

 // DEVICE 1
    new SpanAccessory();
      new UpdateData(DEVICE_ID_1_NAME,DEVICE_ID_1);                 
      new RemoteTempSensor("Temperature Sensor",DEVICE_ID_1);        
      new RemoteHumSensor("Humidity Sensor",DEVICE_ID_1);        
      new RemoteLightSensor("Light Sensor",DEVICE_ID_1);  
      new RemoteBattery(DEVICE_ID_1_NAME,DEVICE_ID_1);    

  // DEVICE 2
    new SpanAccessory();
      new UpdateData(DEVICE_ID_2_NAME,DEVICE_ID_2);                 
      new RemoteTempSensor("Temperature Sensor",DEVICE_ID_2);        
      new RemoteHumSensor("Humidity Sensor",DEVICE_ID_2);        
      new RemoteLightSensor("Light Sensor",DEVICE_ID_2);       
      new RemoteBattery(DEVICE_ID_2_NAME,DEVICE_ID_2);     

  // DEVICE 3
    new SpanAccessory();
      new UpdateData(DEVICE_ID_3_NAME,DEVICE_ID_3);                 
      new RemoteTempSensor("Temperature Sensor",DEVICE_ID_3);        
      new RemoteHumSensor("Humidity Sensor",DEVICE_ID_3);        
      new RemoteLightSensor("Light Sensor",DEVICE_ID_3);  
      new RemoteBattery(DEVICE_ID_3_NAME,DEVICE_ID_3);    


  // DEVICE 4
    new SpanAccessory();
      new UpdateData(DEVICE_ID_4_NAME,DEVICE_ID_4);                 
      new RemoteTempSensor("Temperature Sensor",DEVICE_ID_4);        
      new RemoteHumSensor("Humidity Sensor",DEVICE_ID_4);        
      // new RemoteLightSensor("Light Sensor",DEVICE_ID_4);  
      new RemoteBattery(DEVICE_ID_4_NAME,DEVICE_ID_4);    


  // DEVICE 5
    new SpanAccessory();
      new UpdateData(DEVICE_ID_5_NAME,DEVICE_ID_5);                 
      new RemoteTempSensor("Temperature Sensor",DEVICE_ID_5);        
      new RemoteHumSensor("Humidity Sensor",DEVICE_ID_5);        
      new RemoteLightSensor("Light Sensor",DEVICE_ID_5);  
      new RemoteBattery(DEVICE_ID_5_NAME,DEVICE_ID_5); 

  // DEVICE 6
    new SpanAccessory();
      new UpdateData(DEVICE_ID_6_NAME,DEVICE_ID_6);                 
      new RemoteTempSensor("Temperature Sensor",DEVICE_ID_6);        
      new RemoteHumSensor("Humidity Sensor",DEVICE_ID_6);        
      new RemoteLightSensor("Light Sensor",DEVICE_ID_6);  
      new RemoteBattery(DEVICE_ID_6_NAME,DEVICE_ID_6); 

  // DEVICE 7
    new SpanAccessory();
      new UpdateData(DEVICE_ID_7_NAME,DEVICE_ID_7);                 
      new RemoteTempSensor("Temperature Sensor",DEVICE_ID_7);        
      new RemoteHumSensor("Humidity Sensor",DEVICE_ID_7);        
      new RemoteLightSensor("Light Sensor",DEVICE_ID_7);  
      new RemoteBattery(DEVICE_ID_7_NAME,DEVICE_ID_7); 

  //   // DEVICE 42
  //   new SpanAccessory();
  //     new UpdateData(DEVICE_ID_42_NAME,DEVICE_ID_42);                 
  //     new RemoteTempSensor("Temperature Sensor",DEVICE_ID_42);        
  //     new RemoteHumSensor("Humidity Sensor",DEVICE_ID_42);        
  //     new RemoteLightSensor("Light Sensor",DEVICE_ID_42);  
  //     new RemoteBattery(DEVICE_ID_42_NAME,DEVICE_ID_42);        

//   // DEVICE 8
//     new SpanAccessory();
//       new UpdateData(DEVICE_ID_8_NAME,DEVICE_ID_8);                 
//       new RemoteTempSensor("Temperature Sensor",DEVICE_ID_8);        
//       new RemoteHumSensor("Humidity Sensor",DEVICE_ID_8);        
//       new RemoteLightSensor("Light Sensor",DEVICE_ID_8);  
//       new RemoteBattery(DEVICE_ID_8_NAME,DEVICE_ID_8);

//   // DEVICE 9
//     new SpanAccessory();
//       new UpdateData(DEVICE_ID_9_NAME,DEVICE_ID_9);                 
//       new RemoteTempSensor("Temperature Sensor",DEVICE_ID_9);        
//       new RemoteHumSensor("Humidity Sensor",DEVICE_ID_9);        
//       new RemoteLightSensor("Light Sensor",DEVICE_ID_9);  
//       new RemoteBattery(DEVICE_ID_9_NAME,DEVICE_ID_9);
              
//  // DEVICE 10
//     new SpanAccessory();
//       new UpdateData(DEVICE_ID_10_NAME,DEVICE_ID_10);                 
//       new RemoteTempSensor("Temperature Sensor",DEVICE_ID_10);        
//       new RemoteHumSensor("Humidity Sensor",DEVICE_ID_10);        
//       new RemoteLightSensor("Light Sensor",DEVICE_ID_10);  
//       new RemoteBattery(DEVICE_ID_10_NAME,DEVICE_ID_10); 


//  // DEVICE 11
//     new SpanAccessory();
//       new UpdateData(DEVICE_ID_11_NAME,DEVICE_ID_11);                 
//       new RemoteTempSensor("Temperature Sensor",DEVICE_ID_11);        
//       new RemoteHumSensor("Humidity Sensor",DEVICE_ID_11);        
//       new RemoteLightSensor("Light Sensor",DEVICE_ID_11);  
//       new RemoteBattery(DEVICE_ID_11_NAME,DEVICE_ID_11);    

//   // DEVICE 12
//     new SpanAccessory();
//       new UpdateData(DEVICE_ID_12_NAME,DEVICE_ID_12);                 
//       new RemoteTempSensor("Temperature Sensor",DEVICE_ID_12);        
//       new RemoteHumSensor("Humidity Sensor",DEVICE_ID_12);        
//       new RemoteLightSensor("Light Sensor",DEVICE_ID_12);       
//       new RemoteBattery(DEVICE_ID_12_NAME,DEVICE_ID_12);     

//   // DEVICE 13
//     new SpanAccessory();
//       new UpdateData(DEVICE_ID_13_NAME,DEVICE_ID_13);                 
//       new RemoteTempSensor("Temperature Sensor",DEVICE_ID_13);        
//       new RemoteHumSensor("Humidity Sensor",DEVICE_ID_13);        
//       new RemoteLightSensor("Light Sensor",DEVICE_ID_13);  
//       new RemoteBattery(DEVICE_ID_13_NAME,DEVICE_ID_13);    


//   // DEVICE 14
//     new SpanAccessory();
//       new UpdateData(DEVICE_ID_14_NAME,DEVICE_ID_14);                 
//       new RemoteTempSensor("Temperature Sensor",DEVICE_ID_14);        
//       new RemoteHumSensor("Humidity Sensor",DEVICE_ID_14);        
//       new RemoteLightSensor("Light Sensor",DEVICE_ID_14);  
//       new RemoteBattery(DEVICE_ID_14_NAME,DEVICE_ID_14);    


  // // DEVICE 15
  //   new SpanAccessory();
  //     new UpdateData(DEVICE_ID_15_NAME,DEVICE_ID_15);                 
  //     new RemoteTempSensor("Temperature Sensor",DEVICE_ID_15);        
  //     new RemoteHumSensor("Humidity Sensor",DEVICE_ID_15);        
  //     new RemoteLightSensor("Light Sensor",DEVICE_ID_15);  
  //     new RemoteBattery(DEVICE_ID_15_NAME,DEVICE_ID_15); 

  // // DEVICE 16
  //   new SpanAccessory();
  //     new UpdateData(DEVICE_ID_16_NAME,DEVICE_ID_16);                 
  //     new RemoteTempSensor("Temperature Sensor",DEVICE_ID_16);        
  //     new RemoteHumSensor("Humidity Sensor",DEVICE_ID_16);        
  //     new RemoteLightSensor("Light Sensor",DEVICE_ID_16);  
  //     new RemoteBattery(DEVICE_ID_16_NAME,DEVICE_ID_16); 

  // // DEVICE 17
  //   new SpanAccessory();
  //     new UpdateData(DEVICE_ID_17_NAME,DEVICE_ID_17);                 
  //     new RemoteTempSensor("Temperature Sensor",DEVICE_ID_17);        
  //     new RemoteHumSensor("Humidity Sensor",DEVICE_ID_17);        
  //     new RemoteLightSensor("Light Sensor",DEVICE_ID_17);  
  //     new RemoteBattery(DEVICE_ID_17_NAME,DEVICE_ID_17); 



  // // DEVICE 18
  //   new SpanAccessory();
  //     new UpdateData(DEVICE_ID_18_NAME,DEVICE_ID_18);                 
  //     new RemoteTempSensor("Temperature Sensor",DEVICE_ID_18);        
  //     new RemoteHumSensor("Humidity Sensor",DEVICE_ID_18);        
  //     new RemoteLightSensor("Light Sensor",DEVICE_ID_18);  
  //     new RemoteBattery(DEVICE_ID_18_NAME,DEVICE_ID_18);

  // // DEVICE 19
  //   new SpanAccessory();
  //     new UpdateData(DEVICE_ID_19_NAME,DEVICE_ID_19);                 
  //     new RemoteTempSensor("Temperature Sensor",DEVICE_ID_19);        
  //     new RemoteHumSensor("Humidity Sensor",DEVICE_ID_19);        
  //     new RemoteLightSensor("Light Sensor",DEVICE_ID_19);  
  //     new RemoteBattery(DEVICE_ID_19_NAME,DEVICE_ID_19);      
             
  // // DEVICE 20
  //   new SpanAccessory();
  //     new UpdateData(DEVICE_ID_20_NAME,DEVICE_ID_20);                 
  //     new RemoteTempSensor("Temperature Sensor",DEVICE_ID_20);        
  //     new RemoteHumSensor("Humidity Sensor",DEVICE_ID_20);        
  //     new RemoteLightSensor("Light Sensor",DEVICE_ID_20);       
  //     new RemoteBattery(DEVICE_ID_20_NAME,DEVICE_ID_20);   

  // // DEVICE 21
  //   new SpanAccessory();
  //     new UpdateData(DEVICE_ID_21_NAME,DEVICE_ID_21);                 
  //     new RemoteTempSensor("Temperature Sensor",DEVICE_ID_21);        
  //     new RemoteHumSensor("Humidity Sensor",DEVICE_ID_21);        
  //     new RemoteLightSensor("Light Sensor",DEVICE_ID_21);       
  //     new RemoteBattery(DEVICE_ID_21_NAME,DEVICE_ID_21);         

  // // DEVICE 22
  //   new SpanAccessory();
  //     new UpdateData(DEVICE_ID_22_NAME,DEVICE_ID_22);                 
  //     new RemoteTempSensor("Temperature Sensor",DEVICE_ID_22);        
  //     new RemoteHumSensor("Humidity Sensor",DEVICE_ID_22);        
  //     new RemoteLightSensor("Light Sensor",DEVICE_ID_22);       
  //     new RemoteBattery(DEVICE_ID_22_NAME,DEVICE_ID_22);   

  // // DEVICE 23
  //   new SpanAccessory();
  //     new UpdateData(DEVICE_ID_23_NAME,DEVICE_ID_23);                 
  //     new RemoteTempSensor("Temperature Sensor",DEVICE_ID_23);        
  //     new RemoteHumSensor("Humidity Sensor",DEVICE_ID_23);        
  //     new RemoteLightSensor("Light Sensor",DEVICE_ID_23);       
  //     new RemoteBattery(DEVICE_ID_23_NAME,DEVICE_ID_23);   

  // // DEVICE 42
  // new SpanAccessory();
  //   new UpdateData(DEVICE_ID_42_NAME,DEVICE_ID_42);                 
  //   new RemoteTempSensor("Temperature Sensor",DEVICE_ID_42);        
  //   new RemoteHumSensor("Humidity Sensor",DEVICE_ID_42);        
  //   new RemoteLightSensor("Light Sensor",DEVICE_ID_42);  
  //   new RemoteBattery(DEVICE_ID_42_NAME,DEVICE_ID_42);  
    
  //   // DEVICE 44
  //   new SpanAccessory();
  //     new UpdateData(DEVICE_ID_44_NAME,DEVICE_ID_44);                 
  //     new RemoteTempSensor("Temperature Sensor",DEVICE_ID_44);        
  //     new RemoteHumSensor("Humidity Sensor",DEVICE_ID_44);        
  //     new RemoteLightSensor("Light Sensor",DEVICE_ID_44);  
  //     new RemoteBattery(DEVICE_ID_44_NAME,DEVICE_ID_44);  

  #endif

  #if (USE_MQTT == 1)
    mqtt_espClient.setCACert(root_ca);
    mqtt_client.setServer(mqtt_server, mqtt_port);
    mqtt_client.setCallback(mqtt_callback);
  #endif 

  // magic starts here
  homeSpan.begin(Category::Bridges,HOSTNAME,HOSTNAME);

  char firmware_file[255];
  snprintf(firmware_file,sizeof(firmware_file),"%s/bridge%d.bin",UPDATE_FIRMWARE_HOST,DEVICE_ID);
  Serial.printf("[%s]: firmware file: %s\n",__func__,firmware_file);
  Serial.printf("[%s]: ========== Setup finished ==========\n\n",__func__);
}

void loop()
{
  homeSpan.poll();

  #if (CHECK_FW_ON_BOOT == 1)
    if (wifi_connected() == false) return;
    if (fw_update_on_boot_done == false)
    {
      if (periodic_firmware_check()) fw_update_on_boot_done = true;
    }
  #endif

  #if (USE_MQTT == 1)
    if ((first_heartbeat) and (wifi_connected()) and (mqtt_client.connected()))
    {
      first_heartbeat = false;
      heartbeat();
    }
  #endif
  

  #if (USE_INFLUXDB == 1)
    // check influxdb before first routine - only once
    if (!first_influxdb_check_done)
    {
      // don't print anything here
      // LOG0("[%s]: Checking first time connection with InfluxDB...\n",__func__);
      if (wifi_connected())
      {
        LOG0("[%s]: WiFi OK, Checking first time connection with InfluxDB...\n",__func__);
        if (influxdb_connected())
        {
          first_influxdb_check_done = true;
          WEBLOG("[%s]: Connected to InfluxDB\n",__func__);
          // initial heartbeat - only 1 time
          heartbeat();
        } else 
        {
          LOG0("[%s]: No connection with InfluxDB...\n",__func__);
        }
      } else
      {
        // don't print anything here
        // LOG0("[%s]: WiFi NOT OK\n",__func__);
      }
    }
  #endif

  #if (USE_MQTT == 1)
    if (wifi_connected())
    {
      if (!mqtt_client.connected()) 
      {
        if (   millis() >= (mqtt_reconnect_last_check + (MQTT_CHECK_INTERVAL_S * 1000))   )
        {
           mqtt_reconnect();
           mqtt_reconnect_last_check = millis();
        }
      }
      mqtt_client.loop();
    }
  #endif

  // heartbeat from bridge
  if (millis() >= bridge_heartbeat_interval + (HEARTBEAT_INTERVAL_S * 1000) )
  {
    heartbeat();
    bridge_heartbeat_interval = millis();
  }

  // periodic FW check on server 
  if (millis() >= fw_check_interval + (FW_CHECK_INTERVAL_M * 1000 * 60) )
  {
    periodic_firmware_check();
    fw_check_interval = millis();
  }

}