ESP32 – Network Monitoring

This script runs on an ESP32-WROOM-DA Module

/*
This device will detect the connection of new clients on my home network

A Pushover notification will be sent at the end of the set-up module to publish the name of the device as well as its IP Address.

When a new client is detected, a message is sent to Telegram bot created for this purpose

*/
#include <EEPROM.h>
#include <SPI.h>
#include <WiFi.h>
#include <WiFiManager.h>
#include <WiFiClientSecure.h>                   // part of the esp32 framework V1.0
#include <PubSubClient.h>  // for MQTT
#include <ESPmDNS.h>
#include <WiFiUdp.h>
#include <AsyncUDP.h>
#include <esp_sntp.h>
#include <UniversalTelegramBot.h>               // https://github.com/witnessmenow/Universal-Arduino-Telegram-Bot V1.3.0
#include <string.h>
#include <ArduinoJson.h>
#include <HTTPClient.h>

const char* ssid = "Sotong_Purnama";
const char* password = "15sotong15";
const char* MyHostName = "ESP32 - Network Monitoring";
const char *hostname;  // a variable to store the Hostname of ESP32
const char* ntpServer1 = "pool.ntp.org";
const char* ntpServer2 = "time.nist.gov";
const long  gmtOffset_sec = 28800;  // For GMT+8
const int   daylightOffset_sec = 0;  // No daylight offset

// For Pushover
const char* title;
const char* message;
const char* pushoverUserKey = "u6ysovfgq1nhysszxzh91qnwadch2y";   // Set the user key generated in the Pushover account settings
const char* pushoverAPIToken = "amhb2rxrc2wa8gnpbpek99g2qrh4kx";  // Set the API token generated in the Pushover account settings
//Pushover API endpoint
const char* pushoverApiEndpoint = "https://api.pushover.net/1/messages.json";
const char* messagePrefix = "";  // Set a prefix for all messages



// HTTPS root certificate for api.pushover.net: DigiCert Global Root CA, expires 2031.11.10
const char pushoverCertificateRoot[] = R"=EOF=(
-----BEGIN CERTIFICATE-----
MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD
QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT
MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB
CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97
nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt
43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P
T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4
gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO
BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR
TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw
DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr
hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg
06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF
PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls
YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk
CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=
-----END CERTIFICATE-----
)=EOF=";

WiFiClient espClient;
PubSubClient mqtt_client(espClient);
WiFiClientSecure clientTCP;  // for TELEGRAM

// Create a WiFiClientSecure object
WiFiClientSecure ipClient;

bool wifiConnected = true;

// MQTT broker
const char* mqtt_server = "192.168.86.225";
const unsigned mqtt_port = 1883;
const char* mqtt_user = "homeassistant";
const char* mqtt_password = "raspberrypi";

// TELEGRAM
// token for bot "Devices Presence" created on my phone
char TelegramBOTtoken[50] = "6604978268:AAEIcoq__dhu_iIOiq3E6Xw54olv3-47NHI";
char Chat_ID[15] = "739396707";   // pops telegram ID
char Country[30] = "Asia/Singapore";
char chatId[15] = "";
char telegram_message[200];

// Network Configuration  (Needed if we do not use WiFiManager to connect to Access Point and select WiFiNetwork)
// IPAddress local_IP(192, 168, 86, 222);
// Set your Gateway IP address
// IPAddress gateway(192, 168, 86, 1);
// IPAddress subnet(255, 255, 255, 0);
// IPAddress primaryDNS(8, 8, 8, 8);   // optional
// IPAddress secondaryDNS(8, 8, 4, 4); // optional

UniversalTelegramBot bot(TelegramBOTtoken, clientTCP); // setup the Telegram bot

AsyncUDP udp;

void sendPushover(const char* title, const char* message){
  const char* hostname;
  String IPAddress;
  //Make HTTP POST request to send notification
  if (WiFi.status() == WL_CONNECTED) {
    WiFi.setHostname(MyHostName);
    hostname = WiFi.getHostname();
    IPAddress = WiFi.localIP().toString().c_str();

    // Create a JSON object with notification details
    // Check the API parameters: https://pushover.net/api
    StaticJsonDocument<512> notification;
    notification["token"] = pushoverAPIToken;
    notification["user"] = pushoverUserKey;
    notification["message"] = IPAddress;
    notification["title"] = hostname;
    notification["url"] = "";
    notification["url_title"] = "";
    notification["html"] = "";
    notification["priority"] = "";
    notification["sound"] = "cosmic";
    notification["timestamp"] = "";

    // Serialize the JSON object to a string
    String jsonStringNotification;
    serializeJson(notification, jsonStringNotification);

 
    // Set the certificate
    ipClient.setCACert(pushoverCertificateRoot);

    // Create an HTTPClient object
    HTTPClient https;
    // Specify the target URL
    https.begin(ipClient, pushoverApiEndpoint);
    // Add headers
    https.addHeader("Content-Type", "application/json");

    // Send the POST request with the JSON data
    int httpResponseCode = https.POST(jsonStringNotification);

    // Check the response
    if (httpResponseCode > 0) {
      Serial.printf("HTTPS response code: %d\n", httpResponseCode);
      String response = https.getString();
      Serial.println("Response:");
      Serial.println(response);
    } else {
      Serial.printf("HTTPS response code: %d\n", httpResponseCode);
    }

    // Close the connection
    https.end();
  }
}


void printHex(char *data, int length)
{
	int p = 0;
	while(p < length)
	{
		char ascii[17];
		int i = 0;
		for(; i < 16; i++)
		{
			Serial.printf("%02X ", data[p]);
			if(data[p] >= 32)// || data[p] < 128)
				ascii[i] = data[p];
			else
				ascii[i] = '.';
			p++;
			if(p == length)
			{ i++; break;}
		}
		ascii[i] = 0;
		Serial.println(ascii);
	}
}

void printIP(char *data)
{
	for(int i = 0; i < 4; i++)
	{
		Serial.print((int)data[i]);
		if(i < 3)
			Serial.print('.');
	}
}

const int DHCP_PACKET_CLIENT_ADDR_LEN_OFFSET = 2;
const int DHCP_PACKET_CLIENT_ADDR_OFFSET = 28;

enum State
{
	READY,
	SNIFFING,
	DETECTED
};

volatile State state = READY;
String newMAC;
String newIP;
String newName;

const char *hexDigits = "0123456789ABCDEF";

void printLocalTime()
{
  struct tm timeinfo;
  // it the function getLocalTime fails
  if(!getLocalTime(&timeinfo)){
    // print an error message
    Serial.println("No time available (yet)");
    // and return
    return;
  }
  //else print the Date and time as described below
  // Format "DayOfWeek, Month Date Year Hour:Minutes:Seconds"
  Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
}

// Callback function (get's called when time adjusts via NTP)
void timeavailable(struct timeval *t)
{
  Serial.println("\nGot time adjustment from NTP!");
  printLocalTime();
}

void parsePacket(char* data, int length)
{
	String tempName;
	String tempIP;
	String tempMAC;
  if(state == DETECTED) return;
	Serial.println("DHCP Packet");
	printHex(data, length);
	Serial.print("MAC address: ");
	for(int i = 0; i < data[DHCP_PACKET_CLIENT_ADDR_LEN_OFFSET]; i++){
		if(i < data[DHCP_PACKET_CLIENT_ADDR_LEN_OFFSET] - 1)
    {
			Serial.printf("%02X:", (int)data[DHCP_PACKET_CLIENT_ADDR_OFFSET + i]);
    }
    else
    {
			Serial.printf("%02X", (int)data[DHCP_PACKET_CLIENT_ADDR_OFFSET + i]);
    }
  }
  for(int i = 0; i < data[DHCP_PACKET_CLIENT_ADDR_LEN_OFFSET]; i++)
	{
		tempMAC += hexDigits[(int)data[DHCP_PACKET_CLIENT_ADDR_OFFSET + i] >> 4];
		tempMAC += hexDigits[(int)data[DHCP_PACKET_CLIENT_ADDR_OFFSET + i] & 15];
		if(i < data[DHCP_PACKET_CLIENT_ADDR_LEN_OFFSET] - 1)
    {
			tempMAC += ":";
    }
	}

	Serial.println();
	//parse options
	int opp = 240;
	while(opp < length)
	{
		switch(data[opp])
		{
			case 0x0C:
			{
				Serial.print("Device name: ");
				for(int i = 0; i < data[opp + 1]; i++)
				{
					Serial.print(data[opp + 2 + i]);
					tempName += data[opp + 2 + i];
				}
				Serial.println();
				break;
			}
			case 0x35:
			{
				Serial.print("Packet Type: ");
				switch(data[opp + 2])
				{
					case 0x01:
						Serial.println("Discover");
					break;
					case 0x02:
						Serial.println("Offer");
					break;
					case 0x03:
						Serial.println("Request");
						if(state == READY)
							state = SNIFFING;
					break;
					case 0x05:
						Serial.println("ACK");
					break;
					default:
						Serial.println("Unknown");
				}
				break;
			}
			case 0x32:
			{
				Serial.print("Device IP: ");
				printIP(&data[opp + 2]);
				Serial.println();
				for(int i = 0; i < 4; i++)
				{
					tempIP += (int)data[opp + 2 + i];
					if(i < 3) tempIP += '.';
				}
				break;
			}
			case 0x36:
			{
				Serial.print("Server IP: ");
				printIP(&data[opp + 2]);
				Serial.println();
				break;
			}
			case 0x37:
			{
				Serial.println("Request list: ");
				printHex(&data[opp + 2], data[opp + 1]);
				break;
			}
			case 0x39:
			{
				Serial.print("Max DHCP message size: ");
				Serial.println(((unsigned int)data[opp + 2] << 8) | (unsigned int)data[opp + 3]);
				break;
			}
			case 0xff:
			{
				Serial.println("End of options.");
				opp = length; 
				continue;
			}
			default:
			{
				Serial.print("Unknown option: ");
				Serial.print((int)data[opp]);
				Serial.print(" (length ");
				Serial.print((int)data[opp + 1]);
				Serial.println(")");
				printHex(&data[opp + 2], data[opp + 1]);
			}
		}

		opp += data[opp + 1] + 2;
	}
	if(state == SNIFFING)
	{
		newName = tempName;
		newIP = tempIP;
		newMAC = tempMAC;
		Serial.println("Stored data.");
		state = DETECTED;
	}
	Serial.println();
//  String message = "New device detected!\n\nName: " + tempName + "\nIP Address: " + tempIP + "\nMAC Address: " + tempMAC + "\n";
  if(state == DETECTED and newName != "" and newIP != "" and newMAC != ""){
    String message = "New device detected!\n\nName: " + newName + "\nIP Address: " + newIP + "\nMAC Address: " + newMAC + "\n";
    Serial.print("Telegram Message: ");
    Serial.println(message);
  }
}

// function to setup UDP protocol
void setupUDP()
{
	if(udp.listen(67)) 
	{
		Serial.print("UDP Listening on IP: ");
		Serial.println(WiFi.localIP());
    // upon receipt of a packet on UDP
		udp.onPacket([](AsyncUDPPacket packet) 
		{
			char *data = (char *)packet.data();
			int length = packet.length();
      //parse the packet pointed by *data with a length of 'length'
			parsePacket(data, length);
		});
	};
}

bool isDataStored()
{
	return EEPROM.read(512) != 0;  // was 96
}

String readString(int offset, int len = 32)
{
	String s;
	int p = offset;
	for(int i = 0; i < len; i++)
	{
		char c = EEPROM.read(p++);
		if(c)
			s+=c;
	}
	return s;
}


void readData()
{
	Serial.println("Reading the data reveiced on UDP");
  newMAC = readString(0, 32);
	newIP = readString(32, 32);
	newName = readString(64, 32);
}

void clearData()
{
	Serial.println("Erasing the EEPROM");
  EEPROM.write(512, 0);  // was 96
	EEPROM.commit();
  Serial.println("EEPROM have been erased.");
}

void writeString(String s, int offset, int len = 32)
{
	int p = offset;
	for(int i = 0; i < len; i++)
		if(i < s.length())
			EEPROM.write(p++, s.charAt(i));
		else
			EEPROM.write(p++, 0);
}

void writeData()
{
	writeString(newMAC, 0, 32);
	writeString(newIP, 32, 32);
	writeString(newName, 64, 32);
	EEPROM.write(512, 1);   // was 96
	EEPROM.commit();
}

//#########################//
//          SET-UP         //
//#########################//
void setup() 
{
  
// for Pushover
  ipClient.setCACert(pushoverCertificateRoot);
  
  int timeout = 120;  // seconds
  // select the GPIO pin for connecting the button (default state: OPEN)
  #define TRIGGER_PIN 0    // GPIO 0
  WiFi.mode(WIFI_STA);
  Serial.begin(115200);
  Serial.println("\nStarting");

  pinMode(TRIGGER_PIN,INPUT_PULLUP);

  WiFi.setHostname(MyHostName);

//  WiFi.begin(ssid, password);
  Serial.print("Hostname is: ");
  Serial.println(WiFi.getHostname());

  //  will start by activating an AP "ESP32_AP", with password "12345678"
  WiFiManager wm; 
  //  wm.resetSettings(); 

  // set timeout for the Config Portal
  wm.setConfigPortalTimeout(timeout);

  WiFiManagerParameter custom_telegram_token("telegram_bot_token", "Telegram BOT Token", "Get it from Telegram App",sizeof(TelegramBOTtoken));
  WiFiManagerParameter custom_chat_id("chat_id", "Telegram Chat ID", "739396707",sizeof(Chat_ID));

  wm.addParameter(&custom_telegram_token);
  wm.addParameter(&custom_chat_id);

  bool res;

  res = wm.autoConnect("ESP32_AP", "12345678");
  if(!res){
    Serial.println("Failed to connect");
  }
  else
  {
    Serial.println("Connected!");
  }

  delay(1000);
  EEPROM.begin(512);  //was 128
	if(isDataStored())
	{
		readData();
		clearData();
		ESP.restart();
	}

  // erase the EEPROM of the ESP32
  // for (int i = 0; i < 512; i++) {
  //   EEPROM.write(i, 0);
  // }
  // EEPROM.commit();
  // delay(500);
  // initialize Serial port with 115200 bauds trasmission speed
  Serial.begin(115200);
  Serial.println();
  WiFi.setHostname(MyHostName);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.print("Hostname is: ");
  Serial.println(WiFi.getHostname());
  // Configure static IP Address
  // if (!WiFi.config(local_IP, gateway, subnet, primaryDNS, secondaryDNS))
  // {
  //   Serial.println("STA Failed to configure");
  // }
  // else
  // {
  //   Serial.println("Wifi configuration successful");
  // }
  // delay(1000);
  clientTCP.setCACert(TELEGRAM_CERTIFICATE_ROOT);

  // Lets deal with the user config values
  // Copy the string value
    strncpy(TelegramBOTtoken, custom_telegram_token.getValue(), sizeof(TelegramBOTtoken));
    Serial.print("----> TelegramBOTtoken: ");
    Serial.println(TelegramBOTtoken);

    strncpy(Chat_ID, custom_chat_id.getValue(), sizeof(Chat_ID));
    Serial.print("----> ChatID: ");
    Serial.println(Chat_ID);

  // Serial.println("Attempting to connect to Wifi network...");
  // Wait until connection is established
  // while(WiFi.status() != WL_CONNECTED){
  //   //  Serial.print(WiFi.status());
  //     Serial.print(".");
  //     delay(500);
  //     if (WiFi.status() == WL_CONNECT_FAILED){
  //       Serial.println("\nAttempt to connect has failed! Rebooting now...");
  //       ESP.restart();
  //     }
  // }
  // Print ESP32's IP & HostName
  // Serial.println("\nConnected to the WiFi network");
  // Serial.print("Local ESP32 IP: ");
  // Serial.println(WiFi.localIP());
  // Serial.print("ESP32 HostName: ");
  // Serial.println(WiFi.getHostname());

  // set notification call-back function
  sntp_set_time_sync_notification_cb( timeavailable );

  /**
   * NTP server address could be aquired via DHCP,
   *
   * NOTE: This call should be made BEFORE esp32 aquires IP address via DHCP,
   * otherwise SNTP option 42 would be rejected by default.
   * NOTE: configTime() function call if made AFTER DHCP-client run
   * will OVERRIDE aquired NTP server address
   */
  sntp_servermode_dhcp(1);    // (optional)

  /**
   * This will set configured ntp servers and constant TimeZone/daylightOffset
   * should be OK if your time zone does not need to adjust daylightOffset twice a year,
   * in such a case time adjustment won't be handled automagicaly.
   */
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer1,ntpServer2);

  /**
   * A more convenient approach to handle TimeZones with daylightOffset 
   * would be to specify a environmnet variable with TimeZone definition including daylight adjustmnet rules.
   * A list of rules for your zone could be obtained from https://github.com/esp8266/Arduino/blob/master/cores/esp8266/TZ.h
   */
  //configTzTime(time_zone, ntpServer1, ntpServer2);

  Serial.println();

  // Initializes UDP
	setupUDP();
 	Serial.println(); 

  Serial.println("###########################");
  Serial.println("# Wifi Network Monitoring #");
  Serial.println("###########################");
  // Print out Ready message as well as IP address given by DHCP
	Serial.print("Host Ready: ");
  Serial.println(WiFi.getHostname());
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
  bot.sendMessage(Chat_ID, "ESP32 - Network Monitoring\nStarting now...");

  // calls the sendPushover function
  sendPushover(title, message);

}

//#########################//
//        MAIN LOOP        //
//#########################//
void loop() 
{

  // In case there is a button connected to GPIO 0 (defined in void setup)
  int timeout = 120; // timeout in seconds
  // Has the button been pressed (to change WiFi configuration)
  if (digitalRead(TRIGGER_PIN) == LOW){
    WiFiManager wm;
    wm.resetSettings(); 
    // set timeout for the Config Portal
    wm.setConfigPortalTimeout(timeout);
    if (!wm.startConfigPortal("ESP32_AP")) {
      Serial.println("Failed to connect and hit timeout");
      delay(3000);
      // reset and restart
      ESP.restart();
      delay(5000);
    }
    else
    { 
      Serial.println("Connected!");
    }
  }  // end of button processing


  // if a new detection has occured
	if(state == DETECTED)
	{
//    String message = "New device detected!\nName: " + newName + "\nIP Address: " + newIP + "\nMAC Address: " + newMAC + "\n";
//    Serial.println("Telegram Message:");
//    Serial.println(message);
    // sends the message to Telegram
    bot.sendMessage(Chat_ID, "New device detected!\nName: " + newName + "\nIP Address: " + newIP + "\nMAC Address: " + newMAC + "\n");
//    if (newName == ""){
//      bot.sendMessage(Chat_ID,"The name has not been detected! You may want to run some network tools for security purpose...");
//    }
    //bot.sendMessage(Chat_ID, telegram_message);
		// udp.close();
		// WiFi.disconnect();
		// WiFi.mode(WIFI_MODE_NULL);
		// writeData();
	//	 delay(1000);
		// ESP.restart();
		state = READY;
	}
}