Code Lock with ESP8266 (ESP32 should also work but is untested)

Hardware needed:
    Wemos D1 Mini (ESP8266)
    Some kind of electric lock.
    A connected matrix keypad with digits 0 through 9, size 3x4.
    The matrix is connected to pins D1 through D7:
    D1 to D4 are connected to the rows.
    D5 to D7 are connected to the columns.
    A relay board to control the lock connected to D8.
    Optional reed switch to detect unlocked/locked/door opened/closed connected to D0.
    Additionally, a 5V power supply is required, such as from a USB charger.

Features:
    WiFi connectivity but can run without network connection but chcking valid times will be a problem since ESP8266 has no RTC.
    Admin web page.
    MQTT connectivity for automations with Home Assistant or similar.
    Supports 8 local individual codes, consisting of the digits 0 through 9,* and #, with a maximum length of 8.
    Different codes can be valid at specific hours of the day.
    Unlock the lock using the keypad by entering a valid code, or
    unlock from admin page, or unlock from MQTT.
    If "AlwaysOpenOn" is set, the lock will be unlocked all the time.
    Automatically connects to an MQTT broker if one is configured.
    Unlock and set AlwaysOpen from MQTT.
    The program updates an MQTT server/topic with activity, including timestamps, who unlocked the door, commands published from MQTT and admin page, and input from the keypad.
    You can text a temporary code to a customers mobile and validate the keypresses and unlock via MQTT, as an example.     Optional detection when the lock/door is opened using a reed switch connected to D8. (Switch is detecting changes.)
    Serial interface enabled in USB, 9600 baud.
    OTA updates.

Wi-Fi Connectivity:
    Configure initial Wi-Fi settings via a simple Access Point (AP) and a web page.
    WiFi Settings are stored in non volatile memory.
    
Password-Protected Admin Web Page:
    The admin page features:
    A button to manually unlock the door.
    A button to permanently put the lock in always-unlocked mode".
    Managing codes, their validity periods, and comments/users.     There is a button to delete configuration file if needed which at reboot will create the default config file.
    Configuring the MQTT broker (address, username, password, topic).
    If you have multiple doors, door names should be different, and MQTT topic should be descriptive for ease of identification.
    Changing the admin password for the admin page.
    Forget the WiFi settings which will create an Access Point for 60 seconds
Configuration Storage:
    Configuration is saved in a JSON file on a non-volatile partition, ensuring it persists across reboots or firmware uploads.
    A default configuration is created if no valid configuration file exists or if it becomes corrupted.
    
Default Settings:
    Default code #1: 12345678 , Valid 24 hours around the clock
    Default admin web page password: adminpass
    Default door/lock/MQTT client name: Door     NtpServer pool.ntp.org
    TimeZone PST8PDT,M3.2.0,M11.1.0   See https://gist.github.com/alwynallan/24d96091655391107939 for details about TZ and DST
    RelayPullTime 1000 (mS)

Known issues:
    No https encryption on web page.
    No TLS encryption for MQTT connection.
    
MQTT:
    Topics:
        Door/CodeLock/activity    Shows status
        Door/CodeLock/keypressed  Shows last keypad press
        Door/CodeLock/cmnd        Topic for commands
    
    In Door/CodeLock/activity:   
    Status messages:
        Door unlocked by <name>
        Door unlocked by admin
        Door unlocked by MQTT
        Door unlocked by AlwaysOpen         Reed switch open
        Reed switch closed
        <timestamp> AlwaysOpen is now on.
        <timestamp> AlwaysOpen is now off.

    In Door/CodeLock/cmnd:
    Valid publish commands:
        Unlock          Unlocks door once
        AlwaysOpenOn    Enables AlwaysOpen state
        AlwaysOpenOff   Disables AlwaysOpen state
        0-9,*,#         Enter codes for testing
        SetNtpServer pool.ntp.org
        SetTimeZone PST8PDT,M3.2.0,M11.1.0  
        SetRelayPullTime 1000

#include <arduino.h>
#include <littlefs.h>
#include <arduinojson.h>
#include <esp8266wifi.h>
#include <time.h>  // Include time library
#include <esp8266webserver.h>
#include <elegantota.h>

#include <pubsubclient.h>
#include <wifimanager.h>
#include <ntpclient.h>
#include <keypad.h>

#define JSON_FILENAME "/config.json"

/*
[env:esp12e]
platform = espressif8266
board = esp12e
framework = arduino
lib_deps =
    bbx10/DNSServer
    bblanchon/ArduinoJson
    tzapu/WiFiManager
    arduino-libraries/NTPClient
    knolleary/PubSubClient
    chris--a/Keypad

This is beta code for CodeLock 0.99 with: 
    WiFi connectivity.
    Password protected admin and OTA update web pages.
    MQTT connectivity for automations with Home Assistant or similar.
    Supports 8 multiple individual codes, consisting of the digits 0 through 9, * and #, with a maximum length of 8 digits.
    Different codes can be valid at specific hours of the day.
    Unlock the lock using the keypad by entering a valid code, or
    unlock from admin page, or unlock from MQTT.
    If "AlwaysOpen" is set, the lock will be unlocked all the time.
    Automatically connects to an MQTT broker if one is configured.
    Unlock and set AlwaysOpen from MQTT.
    The program updates an MQTT server/topic with activity, by whom the door was unlocked, commands published from MQTT or admin page, and input from the keypad.
    A reed switch connected to D0 detects changes if used in lock (locked/unlocked) or door (open/closed). 
    Serial interface enabled in USB, 9600 baud.
*/
ESP8266WebServer server(80);
// AsyncWebServer server(80);

// Admin credentials
String adminUser = "admin";

struct Code {
  String code;
  int validFrom;
  int validTo;
  String remark;
  int counter = 0;       // Counter keeping track of correctly entered keypad digits
};

// Default values
const String version = "0.99g";
const char* defaultDoorName = "Door";
const char* defaultAdminPassword = "adminpass";
const char* defaultCode = "12345678";
const int defaultValidFrom = 0;
const int defaultValidTo = 0;
const char* defaultRemark = "Default";
const int relayPin = D8; // GPIO15 (D8) // Using this pin does not activate relay during boot
const int defaultRelayPullTime = 1000;
const int reedSwitchPin = D0;  // GPIO15 (D8) Detects if handle is engaged or door is opened
// const int tz = +2;
const char* defaultNtpServer = "pool.ntp.org"; 
const char* defaultTimeZone = "CET-1CEST,M3.5.0,M10.5.0/3";  // See https://gist.github.com/alwynallan/24d96091655391107939

char topic[300];  // Ensure this buffer is large enough for your topic string
bool alwaysOpen = false;  // State variable
bool unLocked = true;     // State variable, assume unlocked after a power on
bool reedSwitchState;     // Reed switch is closed(true) or opened(false)
bool lastState;           // Track last reed switch state

String mqttServer = "";  // These are configurable/changable options
String mqttUser = "";
String mqttPassword = "";
String mqttTopic = "";
String adminPassword = defaultAdminPassword;
String doorName = defaultDoorName;
String message = "";
int relayPullTime = defaultRelayPullTime;
// const char* ntpServer = defaultNtpServer;
// const char* timeZone = defaultTimeZone;
char ntpServer[50];  // Adjust size as needed
char timeZone[50];

unsigned long lastWifiAttempt = millis();  // Store the last attempt time for WiFi reconnection
const unsigned long wifiRetryInterval = 60000;  // Retry every 60 seconds

WiFiClient espClient;
PubSubClient client(espClient);
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, ntpServer, 0, 60000);  

Code accessCodes[8]; // Create an array to store user codes

// Matris-keypad configuration
const byte ROWS = 4;
const byte COLS = 3;
char keys[ROWS][COLS] = {
  {'1', '2', '3'},
  {'4', '5', '6'},
  {'7', '8', '9'},
  {'*', '0', '#'}
};
byte rowPins[ROWS] = {D1, D2, D3, D4};  // The ESP8266 pins connect to the row pins
byte colPins[COLS] = {D5, D6, D7}; // The ESP8266 pins connect to the column pins
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);

// Function to initialize the JSON file
void initializeJson() {
  // Serial.println("Initializing JSON file...");
  message = "Initializing JSON file...";
  Serial.println(message.c_str());
  snprintf(topic, sizeof(topic), "%s/CodeLock/activity", mqttTopic.c_str());
  client.publish(topic, message.c_str());


  StaticJsonDocument<3096> doc;

  // Populate default values for codes
  JsonArray codesArray = doc.createNestedArray("codes");
  for (int i = 0; i < 8; i++) {
    JsonObject codeObj = codesArray.createNestedObject();
    if (i == 0) { // Default first code
      codeObj["code"] = defaultCode;
      codeObj["validFrom"] = defaultValidFrom;
      codeObj["validTo"] = defaultValidTo;
      codeObj["remark"] = defaultRemark;
    } else {
      codeObj["code"] = "";
      codeObj["validFrom"] = defaultValidFrom;
      codeObj["validTo"] = defaultValidTo;
      codeObj["remark"] = "";
    }
  }

  // Add MQTT and admin settings
  doc["mqttServer"] = "";
  doc["mqttUser"] = "";
  doc["mqttPassword"] = "";
  doc["mqttTopic"] = "";
  doc["adminPassword"] = defaultAdminPassword;
  doc["doorName"] = defaultDoorName;
  // **Add NTP, TimeZone, and Relay Pull Time**
  doc["ntpServer"] = defaultNtpServer;
  doc["timeZone"] = defaultTimeZone;
  doc["relayPullTime"] = defaultRelayPullTime;  // Use a safe default integer

  // Save to LittleFS
  File file = LittleFS.open(JSON_FILENAME, "w");
  if (!file) {
    Serial.println("Failed to create JSON file.");
    return;
  }
  serializeJson(doc, file);
  file.close();
  // Serial.println("JSON file initialized.");
  message = "JSON file initialized.";
  Serial.println(message.c_str());
  snprintf(topic, sizeof(topic), "%s/CodeLock/activity", mqttTopic.c_str());
  client.publish(topic, message.c_str());
}

// Function to load JSON configuration
void loadJson() {
  File file = LittleFS.open(JSON_FILENAME, "r");
  if (!file) {
    Serial.println("JSON file not found. Initializing...");
    initializeJson();
    file = LittleFS.open(JSON_FILENAME, "r");
    if (!file) {
      // Serial.println("Failed to read JSON file after initialization.");
      message = "Failed to read JSON file after initialization.";
      Serial.println(message.c_str());
      snprintf(topic, sizeof(topic), "%s/CodeLock/activity", mqttTopic.c_str());
      client.publish(topic, message.c_str());
      return;
    }
  }

  StaticJsonDocument<3096> doc;
  DeserializationError error = deserializeJson(doc, file);
  if (error) {
    message = "Failed to parse JSON. Reinitializing...";
    Serial.println(message.c_str());
    snprintf(topic, sizeof(topic), "%s/CodeLock/activity", mqttTopic.c_str());
    client.publish(topic, message.c_str());
    initializeJson();
    return;
  }

  // Load codes
  JsonArray codesArray = doc["codes"];
  for (size_t i = 0; i < codesArray.size(); i++) {
    JsonObject codeObj = codesArray[i];
    accessCodes[i].code = codeObj["code"].as<string>();
    accessCodes[i].validFrom = codeObj["validFrom"];
    accessCodes[i].validTo = codeObj["validTo"];
    accessCodes[i].remark = codeObj["remark"].as<string>();
  }

  // Load MQTT settings
  mqttServer = doc["mqttServer"].as<string>();
  mqttUser = doc["mqttUser"].as<string>();
  mqttPassword = doc["mqttPassword"].as<string>();
  mqttTopic = doc["mqttTopic"].as<string>();
  adminPassword = doc["adminPassword"].as<string>();  // Load admin password
  doorName = doc["doorName"].as<string>();     // Load door name
  // Load NTP & TimeZone with Safety Checks
  strlcpy(ntpServer, doc["ntpServer"] | defaultNtpServer, sizeof(ntpServer));
  strlcpy(timeZone, doc["timeZone"] | defaultTimeZone, sizeof(timeZone));
  // Ensure relayPullTime is a valid integer
  relayPullTime = doc["relayPullTime"].is<int>() ? doc["relayPullTime"].as<int>() : defaultRelayPullTime;

  file.close();
}

// Function to handle the /opendoor request
void handleOpenDoor() {
  unlockDoor();  // Call the unlock routine
  server.send(200, "text/plain", "Door unlocked successfully");  // Send response
  String timestamp = getFormattedTime();
  message = "Door unlocked by admin";
  Serial.print(timestamp.c_str());
  Serial.print(" ");
  Serial.println(message.c_str());
  snprintf(topic, sizeof(topic), "%s/CodeLock/activity", mqttTopic.c_str());
  client.publish(topic, message.c_str());
}

// Function to handle the /deleteConfig request
void handleDeleteConfig() {
  deleteConfig();  // Call the delete routine
  server.send(200, "text/plain", "JSON config file deleted successfully");  // Send response
  message = "JSON config file deleted from admin page.";
  Serial.println(message.c_str());
  snprintf(topic, sizeof(topic), "%s/CodeLock/activity", mqttTopic.c_str());
  // client.publish(topic, message.c_str());
}

// Function to handle the /reboot request
void handleReboot() {
  server.send(200, "text/plain", "Rebooting....");  // Send response
  message = "Rebooting....";
  Serial.println(message.c_str());
  snprintf(topic, sizeof(topic), "%s/CodeLock/activity", mqttTopic.c_str());
  client.publish(topic, message.c_str());
  delay(500);
  reBoot();  // Call the Reboot routine
}

// Function to handle the /alwaysOpen request
void handleAlwaysOpen() {
  alwaysOpen = !alwaysOpen;  // Toggle state

  String timestamp = getFormattedTime();
  if (alwaysOpen == true) {
    message = timestamp + " AlwaysOpen is now on.";
    lastState = !reedSwitchState;
  } else {
    message = timestamp + " AlwaysOpen is now off.";
  }
  Serial.print(timestamp.c_str());
  Serial.print(" ");    
  Serial.println(message.c_str());
  snprintf(topic, sizeof(topic), "%s/CodeLock/activity", mqttTopic.c_str());
  client.publish(topic, message.c_str());

  server.send(200, "text/plain", alwaysOpen ? "Always Open Enabled" : "Always Open Disabled");
//server.sendContent(""); // Sends no content, but toggles as long button is pressed.

}

// Function to handle configuration page
void handleConfigPage() {
  // Basic authentication handler
  if (!server.authenticate(adminUser.c_str(), adminPassword.c_str())) {
    return server.requestAuthentication();
  }
  String timestamp = getFormattedTime();
  String html = "";
  html += "<h1>" + doorName + " CodeLock Configuration"; "</h1>";
  html += "<h6>" + version + " " + timestamp; "</h6>";
  // html += "<br>";
  // html += "<h6>" " relayPullTime(mS): " + relayPullTime; "</h6>";
  
  // Existing form fields for codes, MQTT settings, etc.
  html += "<form action="\"/save\"" method="\"post\"">";  // Correct form start

  // New buttons with properly escaped attributes
  // html += "<button type="\"button\"" onclick="\"saveConfig()\"">Save</button>";
  html += "<button type="\"button\"" id="\"toggleLockBtn\"" onclick="\"openDoor()\"">Unlock</button>";


  // Button with dynamic text based on `alwaysOpen` state
  html += "<button type="\"button\"" id="\"alwaysOpenBtn\"" onclick="\"toggleAlwaysOpen()\"">"
          + String(alwaysOpen ? "Disable Always Unlocked" : "Enable Always Unlocked") + "</button>";


  html += "<button type="\"button\"" onclick="\"deleteConfig()\"">Delete Config File</button>";
  html += "<button type="\"button\"" onclick="\"reBoot()\"">Reboot</button>";

  // JavaScript functions for buttons
  html += "<script>";
  
  // JavaScript functions for Open Door button
  html += "function openDoor() {";
  html += "  fetch('/opendoor')";
  html += "  .then(response => response.text())";
  html += "  .then(text => {";
  html += "    updateToggleLockBtn(text);";  // Update button text and color
  html += "  });";
  html += "}";

   
  html += "function updateToggleLockBtn(state) {";
  html += "  let btn = document.getElementById('updateToggleLockBtn');";
  html += "  if (state.includes('Enabled')) {";
  html += "    btn.innerText = 'Disable Always Unlocked';";
  html += "    btn.style.backgroundColor = '#77dd77';";  // Red when enabled
  html += "  } else {";
  html += "    btn.innerText = 'Enable Always Unlocked';";
  html += "    btn.style.backgroundColor = '#ff6961';";  // Green when disabled
  html += "  }";
  html += "}";


  // JavaScript to toggle the Always Open button
  html += "function toggleAlwaysOpen() {";
  html += "  fetch('/alwaysopen')";  // Send request to toggle state
  html += "  .then(response => response.text())";
  html += "  .then(text => {";
  html += "    alert(text);";  // Display the status message
  html += "    updateButton(text);";  // Update button text and color
  html += "  });";
  html += "}";

  // Function to update the AlwaysOpen button based on response
  html += "function updateButton(state) {";
  html += "  let btn = document.getElementById('alwaysOpenBtn');";
  html += "  if (state.includes('Enabled')) {";
  html += "    btn.innerText = 'Disable Always Unlocked';";
  html += "    btn.style.backgroundColor = '#77dd77';";  // Red when enabled
  html += "  } else {";
  html += "    btn.innerText = 'Enable Always Unlocked';";
  html += "    btn.style.backgroundColor = '#ff6961';";  // Green when disabled
  html += "  }";
  html += "}";

  // JavaScript functions for Delete Config button
  html += "function deleteConfig() {";
  html += "  fetch('/deleteconfig')";
  html += "  .then(response => response.text())";
  html += "  .then(alert);";
  html += "}";

  html += "function forgetWiFi() {";
  html += "  if (confirm('Are you sure you want to forget the current WiFi settings? This will restart the device.')) {";
  html += "    fetch('/forgetwifi')";
  html += "    .then(response => response.text())";
  html += "    .then(alert);";
  html += "  }";
  html += "}";

  // JavaScript functions for Reboot button
  html += "function reBoot() {";
  html += "  fetch('/reboot')";
  html += "  .then(response => response.text())";
  html += "  .then(alert);";
  html += "}";

  html += "window.onload = function() {";
  html += "  updateButton('" + String(alwaysOpen ? "Enabled" : "Disabled") + "');";
  html += "}";
  
  html += "</script>";
  
  // Form fields for codes
  for (int i = 0; i < 8; i++) {
    html += "<h6><br></h6>";
    html += "Code" + String(i + 1) + ": <input type="\"text\"" name="\"code"" +="" string(i)="" "\"="" value="\""" accesscodes[i].code="" maxlength="\"8\""><br>";
    html += "Valid From (0[:00]-23[:00]): <input type="\"number\"" name="\"validFrom"" +="" string(i)="" "\"="" value="\""" string(accesscodes[i].validfrom)="" min="\"0\"" max="\"23\""><br>";
    html += "Valid To (0[:00]-23[:00]): <input type="\"number\"" name="\"validTo"" +="" string(i)="" "\"="" value="\""" string(accesscodes[i].validto)="" min="\"0\"" max="\"23\""><br>";
    html += "Remark: <input type="\"text\"" name="\"remark"" +="" string(i)="" "\"="" value="\""" accesscodes[i].remark="" maxlength="\"10\""><br>";
  }


  // NTP and Time Settings fields
  html += "<h3>NTP & Time Settings</h3>";
  html += "NTP Server: <input type="\"text\"" name="\"ntpServer\"" value="\""" +="" string(ntpserver)="" "\"=""><br>";
  html += "Time Zone: <input type="\"text\"" name="\"timeZone\"" value="\""" +="" string(timezone)="" "\"=""><br>";
  html += "Relay Pull Time (ms): <input type="\"number\"" name="\"relayPullTime\"" value="\""" +="" string(relaypulltime)="" "\"="" min="\"100\"" max="\"5000\""><br>";


  // MQTT settings fields
  html += "<h3>MQTT Settings</h3>";
  html += "Server: <input type="\"text\"" name="\"mqttServer\"" value="\""" +="" mqttserver="" "\"=""><br>";
  html += "User: <input type="\"text\"" name="\"mqttUser\"" value="\""" +="" mqttuser="" "\"=""><br>";
  html += "Password: <input type="\"password\"" name="\"mqttPassword\"" value="\""" +="" mqttpassword="" "\"=""><br>";
  html += "Topic: <input type="\"text\"" name="\"mqttTopic\"" value="\""" +="" mqtttopic="" "\"=""><br>";

  // Admin password field
  html += "<h3>Admin Password</h3>";
  html += "Password: <input type="\"text\"" name="\"adminPassword\"" value="\""" +="" adminpassword="" "\"=""><br>";

  html += "<h3>Door Name</h3>";  // Door name field
  html += "Door Name: <input type="\"text\"" name="\"doorName\"" value="\""" +="" doorname="" "\"=""><br>";

  html += "<br><input type="\"submit\"" value="\"Save\""></form>";  // Submit button

  html += "<button type="\"button\"" onclick="\"forgetWiFi()\"">Forget WiFi</button>"; // Forget WiFi button
  html += "";
  server.send(200, "text/html", html);
}


// Function to save configuration
void handleSave() {
  StaticJsonDocument<3096> doc;
  Serial.println("Saving JSON file.");

  JsonArray codesArray = doc.createNestedArray("codes");
  for (int i = 0; i < 8; i++) {
    JsonObject codeObj = codesArray.createNestedObject();
    codeObj["code"] = server.arg("code" + String(i));
    codeObj["validFrom"] = server.arg("validFrom" + String(i)).toInt();
    codeObj["validTo"] = server.arg("validTo" + String(i)).toInt();
    codeObj["remark"] = server.arg("remark" + String(i));
  }

  doc["mqttServer"] = server.arg("mqttServer");
  doc["mqttUser"] = server.arg("mqttUser");
  doc["mqttPassword"] = server.arg("mqttPassword");
  doc["mqttTopic"] = server.arg("mqttTopic");
  doc["adminPassword"] = server.arg("adminPassword");
  doc["doorName"] = server.arg("doorName");
  doc["ntpServer"] = server.arg("ntpServer");
  doc["timeZone"] = server.arg("timeZone");
  // Ensure valid relayPullTime
  int tempRelayPullTime = server.arg("relayPullTime").toInt();
  if (tempRelayPullTime <= 0) {
    tempRelayPullTime = defaultRelayPullTime;  // Reset to default if invalid
  }
  doc["relayPullTime"] = tempRelayPullTime;

  File file = LittleFS.open(JSON_FILENAME, "w");
  if (!file) {
    server.send(500, "text/plain", "Failed to save configuration.");
    return;
  }

  serializeJson(doc, file);
  file.close();

  loadJson(); // Reload saved configuration
  server.sendHeader("Location", "/");
  server.send(303);
}

void SerPrintAndPubMess(String mess) {
  String timestamp = getFormattedTime();
  Serial.print(timestamp.c_str());
  Serial.print(" ");
  Serial.println(mess.c_str());
  snprintf(topic, sizeof(topic), "%s/CodeLock/activity", mqttTopic.c_str());
  client.publish(topic, mess.c_str());
}

void unlockDoor() {
  Serial.println(relayPullTime);
  digitalWrite(relayPin, HIGH);  // Pull relay
  delay(relayPullTime);                    // in milliSeconds.
  digitalWrite(relayPin, LOW);   // Release relay
  unLocked = true;  // True until reedSwitch indicates door handle has been pressed, or door has been opened.
}

void deleteConfig() {
  if (!LittleFS.begin()) {
    Serial.println("Failed to mount LittleFS.");
    return;
  }
  // Check if the file exists
  if (LittleFS.exists(JSON_FILENAME)) {
    // Delete the file
    if (LittleFS.remove(JSON_FILENAME)) {
      Serial.println("JSON file deleted successfully.");
    } else {
      Serial.println("Failed to delete JSON file.");
    }
  } else {
    Serial.println("JSON file does not exist.");
  }
  loadJson();
}

void handleForgetWiFi() {
    WiFi.disconnect(true);
    server.send(200, "text/plain", "WiFi credentials erased. Restarting...");
    delay(1000);
    ESP.restart();
}

void reBoot() {
  Serial.println("Rebooting...");
  delay(100);  // Short delay to allow the message to be printed
  ESP.restart();  // Reboot the ESP8266
}

// MQTT callback for commands
void callback(char* topic, byte* payload, unsigned int length) {
  String command = "";
  for (int i = 0; i < length; i++) {
    command += (char)payload[i];
  }

    if (command == "Unlock") {
      unlockDoor();
      String timestamp = getFormattedTime();
      message = "Door unlocked by MQTT";
      Serial.print(timestamp.c_str());
      Serial.print(" ");
      Serial.println(message.c_str());

      // Use a different topic buffer here
      char pub_topic[200];  // Publish topic buffer
      snprintf(pub_topic, sizeof(pub_topic), "%s/CodeLock/activity", mqttTopic.c_str());
      client.publish(pub_topic, message.c_str());

    }
  if (command == "AlwaysOpenOn") {
    alwaysOpen = true;
    lastState = !reedSwitchState;
    String timestamp = getFormattedTime();
    message = "AlwaysOpenOn set by MQTT";
    Serial.print(timestamp.c_str());
    Serial.print(" ");
    Serial.println(message.c_str());

    // Use a different topic buffer here
    char pub_topic[200];  // Publish topic buffer
    snprintf(pub_topic, sizeof(pub_topic), "%s/CodeLock/activity", mqttTopic.c_str());
    client.publish(pub_topic, message.c_str());

  }
  if (command == "AlwaysOpenOff") {
    alwaysOpen = false;
    String timestamp = getFormattedTime();
    message = "AlwaysOpenOff set via MQTT";
    Serial.print(timestamp.c_str());
    Serial.print(" ");
    Serial.println(message.c_str());

    // Use a different topic buffer here
    char pub_topic[200];  // Publish topic buffer
    snprintf(pub_topic, sizeof(pub_topic), "%s/CodeLock/activity", mqttTopic.c_str());
    client.publish(pub_topic, message.c_str());

  }
  // Receiving a single char of 0-9,*,# will be accepted as buttons pressed
  if (command.length() == 1 && (isdigit(command.charAt(0)) || command.charAt(0) == '*' || command.charAt(0) == '#')) {
    String timestamp = getFormattedTime();
    message = "Keypress " + command + " received via MQTT";
    Serial.print(timestamp.c_str());
    Serial.print(" ");
    Serial.println(message.c_str());

    // Use a different topic buffer here
    char pub_topic[200];  // Publish topic buffer
    snprintf(pub_topic, sizeof(pub_topic), "%s/CodeLock/activity", mqttTopic.c_str());
    client.publish(pub_topic, message.c_str());
    char key = command.charAt(0);
    handleKeyInput(key);
  }

  if (command.startsWith("SetTimeZone ")) { // Check if the command starts with "SetTimeZone "
    String tzValue = command.substring(12); // Extract everything after "SetTimeZone "

    // Convert String to char array
    static char newTimeZone[50];  // Buffer to store new timezone
    tzValue.toCharArray(newTimeZone, sizeof(newTimeZone));
    strlcpy(timeZone, newTimeZone, sizeof(timeZone));

    String timestamp = getFormattedTime();
    message = "TimeZone changed by MQTT to " + String(timeZone); // Reply with info
    Serial.print(timestamp.c_str());
    Serial.print(" ");
    Serial.println(message.c_str());

    // Use a different topic buffer here
    char pub_topic[200];  // Publish topic buffer
    snprintf(pub_topic, sizeof(pub_topic), "%s/CodeLock/activity", mqttTopic.c_str());
    client.publish(pub_topic, message.c_str());

    // Apply the new timezone setting
    configTime(timeZone, ntpServer);

    delay(2000); // Wait a moment to allow time sync
    struct tm timeinfo;
    if (!getLocalTime(&timeinfo)) { 
        Serial.println("Failed to set new timezone, reverting to default.");
        
        // timeZone = defaultTimeZone;  // Reset to default
        strlcpy(timeZone, defaultTimeZone, sizeof(timeZone));
        configTime(timeZone, ntpServer);

        // Notify about the fallback
        message = "TimeZone change failed, reverted to default: " + String(defaultTimeZone);
        Serial.println(message);
        client.publish(pub_topic, message.c_str());
    } else {
        Serial.println("Timezone updated successfully.");
    }
  }

  if (command.startsWith("SetNtpServer ")) { // Check if the command starts with "SetNtpServer "
    String ntpServerValue = command.substring(13); // Extract everything after "SetNtpServer "

    // Convert String to char array
    static char newNtpServer[50];  // Buffer to store new NtpServer
    ntpServerValue.toCharArray(newNtpServer, sizeof(newNtpServer));
    strlcpy(ntpServer, newNtpServer, sizeof(ntpServer));  // Update ntpServer pointer

    String timestamp = getFormattedTime();
    message = "NtpServer changed by MQTT to " + String(ntpServer); // Reply with info
    Serial.print(timestamp.c_str());
    Serial.print(" ");
    Serial.println(message.c_str());

    // Use a different topic buffer here
    char pub_topic[200];  // Publish topic buffer
    snprintf(pub_topic, sizeof(pub_topic), "%s/CodeLock/activity", mqttTopic.c_str());
    client.publish(pub_topic, message.c_str());

    // Apply the new timezone setting
    configTime(timeZone, ntpServer);

    delay(2000); // Wait a moment to allow time sync
    struct tm timeinfo;
    if (!getLocalTime(&timeinfo)) { 
        Serial.println("Failed to set new NtpServer, reverting to default.");
        
        strlcpy(ntpServer, defaultNtpServer, sizeof(ntpServer));  // Reset to default
        configTime(timeZone, ntpServer);

        // Notify about the fallback
        message = "NtpServer change failed, reverted to default: " + String(defaultNtpServer);
        Serial.println(message);
        client.publish(pub_topic, message.c_str());
    } else {
        Serial.println("NtpServer updated successfully.");
    }
  }

  
  if (command.startsWith("SetRelayPullTime ")) { // Check if the command starts with "SetRelayPullTime "
    String relayPullTimeValue = command.substring(17); // Extract value after the command

    // Convert String to integer safely
    relayPullTime = relayPullTimeValue.toInt(); 

    String timestamp = getFormattedTime();
    message = "RelayPullTime changed by MQTT to " + String(relayPullTime); // Reply with info
    Serial.print(timestamp.c_str());
    Serial.print(" ");
    Serial.println(message.c_str());

    // Use a different topic buffer here
    char pub_topic[200];  // Publish topic buffer
    snprintf(pub_topic, sizeof(pub_topic), "%s/CodeLock/activity", mqttTopic.c_str());
    client.publish(pub_topic, message.c_str());
  }
}

// MQTT reconnect
void reconnect() {
    String clientId = "CodeLock-" + doorName; // Unique Client ID
    if (client.connect(clientId.c_str(), mqttUser.c_str(), mqttPassword.c_str())) {

//  if (client.connect("%sCodeLock", mqttUser.c_str(), mqttPassword.c_str())) {
      char sub_topic[300];  // Subscription topic buffer
      snprintf(sub_topic, sizeof(sub_topic), "%s/CodeLock/cmnd", mqttTopic.c_str());
//    snprintf(sub_topic, sizeof(sub_topic), "%s/CodeLock/%s/cmnd", mqttTopic.c_str(), doorName.c_str()); // unique cmnd topic?
      client.subscribe(sub_topic);  // Subscribe using sub_topic
      snprintf(topic, sizeof(topic), "%s/CodeLock/activity", mqttTopic.c_str());
      client.publish(topic, "Connected to MQTT");
    }
}


// Is now a valid time for this code?
bool isValidTime(int startHour, int endHour) {
    int currentHour = getFormattedTime().substring(11, 13).toInt();
    if (endHour >= startHour) {  // Valid period does not cross midnight
      if (currentHour >= startHour && currentHour < endHour) { // Time is after startTime and before stopTime
        return true;
      }
    }
    if (endHour <= startHour) {  // Valid period cross midnight
      if (currentHour >= startHour && currentHour < 24) { // Time is before midnight
        return true;
      }
      if (currentHour >= 0 && currentHour < endHour) { // Time is after midnight
        return true;
      }
    }
  Serial.println("startHour: " + String(startHour) + " currentHour: " + String(currentHour) + " endHour: " + String(endHour));
  return false;
}


void handleKeyInput(char key) {
  bool codeMatched = false;
  Serial.print("Key pressed: ");
  Serial.println(key);

  // Make single keypress to string and publish
  char keyStr[2];           // Buffer to hold the character and a null terminator
  keyStr[0] = key;          // Store the character
  keyStr[1] = '\0';         // Null terminator to make it a valid C-string
  // topic = String(mqttTopic.c_str()) + "/CodeLock/keypressed";
  snprintf(topic, sizeof(topic), "%s/CodeLock/keypressed", mqttTopic.c_str());
  client.publish(topic, keyStr);

  for (int i = 0; i < sizeof(accessCodes) / sizeof(accessCodes[0]); i++) {

    // Check if the key matches the expected character in the code sequence
    if (key == accessCodes[i].code[accessCodes[i].counter]) {
      accessCodes[i].counter++;  // Move to the next character
      Serial.print("Correct input for code ");
      Serial.print(i);
      Serial.print(". Counter: ");
      Serial.println(accessCodes[i].counter);

      // Check if the entire code has been entered correctly
      if (accessCodes[i].counter == accessCodes[i].code.length()) {
        if (isValidTime(accessCodes[i].validFrom, accessCodes[i].validTo)) {
          unlockDoor();  // Unlock if time is valid
          message = "Door unlocked by " + accessCodes[i].remark;
          //SerPrintAndPubMess(message); //Fråga ch varför detta inte funkar
          String timestamp = getFormattedTime();
          Serial.print(timestamp.c_str());
          Serial.print(" ");
          Serial.println(message.c_str());
          snprintf(topic, sizeof(topic), "%s/CodeLock/activity", mqttTopic.c_str());
          client.publish(topic, message.c_str());
          
          accessCodes[i].counter = 0;  // Reset counter after successful entry
          codeMatched = true;
        } else {
          String timestamp = getFormattedTime();
          message = "Code valid for " + accessCodes[i].remark + ", but not within allowed time. (" + timestamp + ")" ;
          Serial.println(message.c_str());
          snprintf(topic, sizeof(topic), "%s/CodeLock/activity", mqttTopic.c_str());
          client.publish(topic, message.c_str());
        }
      }
    } else {
      // Incorrect input, check if it matches the first digit of this code
      if (key == accessCodes[i].code[0]) {
        accessCodes[i].counter = 1; // Restart from the first digit
        Serial.print("Incorrect input, but matches first digit. Restarting counter for code ");
        Serial.println(i);
      } else {
        // Fully incorrect input, reset counter for this code
        if (accessCodes[i].counter > 0) {  // Only print if counter was non-zero
          Serial.print("Incorrect input. Resetting counter for code ");
          Serial.println(i);
        }
        accessCodes[i].counter = 0;
      }
    }
  }

  if (!codeMatched) {
    Serial.println("No code matched. Waiting for next input.");
  }
}

void setup() {
  Serial.begin(9600);
  unsigned long startTime = millis();
  while (!Serial && (millis() - startTime) < 3000) {  // Wait for 3 seconds
    // Optional: Blink LED to indicate waiting
  }
  if (Serial) {
    Serial.println("Serial monitor detected");
  } else {
    // Proceed without serial connection
    delay(3500);
  }

  message.reserve(300);  // Reserve space for up to 300 characters

  pinMode(relayPin, OUTPUT);
  digitalWrite(relayPin, LOW);
  // pinMode(reedSwitchPin, INPUT_PULLUP); // Pin connected via reedswitch to ground
  pinMode(reedSwitchPin, INPUT_PULLDOWN_16); // Pin connected via reedswitch to 3.3V or via resistor to Vcc
  reedSwitchState = digitalRead(reedSwitchPin); //Check if Reed switch is closed(true) or opened(false)
  lastState = reedSwitchState; // Save present state to see if state changes later.

  if (!LittleFS.begin()) {
    if (Serial) { Serial.println("Failed to mount LittleFS."); }
    return;
  }

  loadJson();

  //   // Wi-Fi & AP-konfiguration
  //   WiFiManager wifiManager;
  //   wifiManager.autoConnect("CodeLock-AP");  // Startar AP om nätverk saknas

  // while (WiFi.status() != WL_CONNECTED) {
  //   delay(500);
  //   Serial.print(".");
  // }

  // Wi-Fi & AP configuration
  WiFiManager wifiManager;
  // wifiManager.autoConnect("CodeLock-AP");  // Starts AP if no network is found

  wifiManager.setConfigPortalTimeout(60);  // 1-minute timeout for AP mode

  if (!wifiManager.autoConnect("CodeLock-AP")) {
    Serial.println("Failed to connect and no configuration entered. Continuing...");
  }


  // Non-blocking WiFi connection attempt
  WiFi.begin();
  unsigned long wifiTimeout = millis() + 10000; // 10-second timeout

  while (WiFi.status() != WL_CONNECTED && millis() < wifiTimeout) {
      delay(500);
      Serial.print(".");
  }

  // If WiFi is not connected, continue without blocking
  if (WiFi.status() == WL_CONNECTED) {
      Serial.println("\nWiFi connected!");
  } else {
      Serial.println("\nWiFi not connected, running in offline mode.");
  }


  // if (Serial) { 
  //   Serial.println("\nWiFi connected!"); 
  //   }
  Serial.println(adminPassword.c_str());
  Serial.println(mqttServer.c_str());
  Serial.println(mqttUser.c_str());
  Serial.println(mqttPassword.c_str());
  Serial.println(mqttTopic.c_str());

  Serial.println("");
  Serial.println(version);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
  Serial.println(ntpServer);
  Serial.println(timeZone);
  Serial.println(relayPullTime);


  ElegantOTA.begin(&server);    // Start ElegantOTA, http://<ip address="">/update
  ElegantOTA.setAuth(adminUser.c_str(), adminPassword.c_str());  // Set Authentication Credentials
  server.begin();
  Serial.println("OTA HTTP server started");

  // Connect to MQTT
  client.setServer(mqttServer.c_str(), 1883);
  client.setCallback(callback);
  String topic = "CodeLock";

  // Configure time zone and start NTP
  configTime(0, 0, ntpServer);
  setenv("TZ", timeZone, 1);
  tzset();

  server.on("/", handleConfigPage);
  server.on("/save", HTTP_POST, handleSave);
  // Define the routine for unlocking the door
  server.on("/opendoor", HTTP_GET, handleOpenDoor);
  // Define the routine for deleting the JSON config file
  server.on("/deleteconfig", HTTP_GET, handleDeleteConfig);
  // Define the routine for reboot
  server.on("/reboot", HTTP_GET, handleReboot);
  // Define the routine for the alwaysOpen state
  server.on("/alwaysopen", HTTP_GET, handleAlwaysOpen);
  server.on("/forgetwifi", HTTP_GET, handleForgetWiFi); // Forget WiFi routine

  server.begin();  // Start the server

  String timestamp = getFormattedTime();
  message = timestamp + " HTTP server started:80";
  Serial.println(message.c_str());
}

// Function to print formatted time
String getFormattedTime() {
    time_t now;
    struct tm timeinfo;
    time(&now);
    localtime_r(&now, &timeinfo);
    char buffer[20];
    strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &timeinfo);
    return String(buffer);
}

void loop() {
  // Check if WiFi is lost and retry every 60 seconds
  if (WiFi.status() != WL_CONNECTED && millis() - lastWifiAttempt >= wifiRetryInterval) {
      Serial.println("WiFi lost, attempting to reconnect...");
      WiFi.begin();
      lastWifiAttempt = millis();
  }


  if (!client.connected()) {
    reconnect();
  }
  client.loop();
  server.handleClient();  // Handle webrequests

// Handle state of Reed Switch
  reedSwitchState = digitalRead(reedSwitchPin); //Check if Reed switch is closed(true) or opened(false)

  if (alwaysOpen == true && reedSwitchState != lastState) {  // Reed switch has changed state
    // alwaysOpen is set and reed switch has changed, so unlock door again
    String timestamp = getFormattedTime();
    message = "Door unlocked by AlwaysOpen";
    Serial.print(timestamp.c_str());
    Serial.print(" ");
    Serial.println(message.c_str());
    snprintf(topic, sizeof(topic), "%s/CodeLock/activity", mqttTopic.c_str());
    client.publish(topic, message.c_str());
    //delay(200);
    unlockDoor();  // Call existing unlock function
  }

  if (reedSwitchState == false && reedSwitchState != lastState) {  // Reed switch has changed state
    // ReedSwitch changed state and is now open.
    String timestamp = getFormattedTime();
    message = "Reed switch open";
    Serial.print(timestamp.c_str());
    Serial.print(" ");
    Serial.println(message.c_str());
    snprintf(topic, sizeof(topic), "%s/CodeLock/activity", mqttTopic.c_str());
    client.publish(topic, message.c_str());
    //delay(200);
  }

  if (reedSwitchState == true && reedSwitchState != lastState) {  // Reed switch has changed state
    //ReedSwitch changed state, reed switch is now closed
    String timestamp = getFormattedTime();
    message = "Reed switch closed";
    Serial.print(timestamp.c_str());
    Serial.print(" ");
    Serial.println(message.c_str());
    snprintf(topic, sizeof(topic), "%s/CodeLock/activity", mqttTopic.c_str());
    client.publish(topic, message.c_str());
  }

  //delay(200);
  lastState = reedSwitchState; // Save state to compare with if something changes later.

// Repeatedly read buttons
  char key = keypad.getKey();
  if (key) {
    handleKeyInput(key);
  }
}</ip></int></int></string></string></string></string></string></string></string></string></keypad.h></ntpclient.h></wifimanager.h></pubsubclient.h></elegantota.h></esp8266webserver.h></time.h></esp8266wifi.h></arduinojson.h></littlefs.h></arduino.h>