Code Lock with ESP8266

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.
    Admin web page.
    MQTT connectivity for automations with Home Assistant or similar.
    Supports 8 multiple individual codes, consisting of the digits 0 through 9, 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, including timestamps, who unlocked the door, commands published from MQTT and admin page, and input from the keypad.
    Optional detection when the lock is in the unlocked state using a reed switch connected to D8. (Switch is assumed closed when the door is locked.)
    You can detect if the door is opened/closed instead, if there is trouble detecting the locked/unlocked state of your lock.
    Serial interface enabled in USB, 9600 baud.

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.     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 name and MQTT topic can be set for ease of identification.
    Changing the admin password for the admin page.
    
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
    Default admin web page password: adminpass
    Default door/lock name: Door

Known issues:
    Some unidentified bugs in admin page when adding or changing codes or settings;
    Always refresh and only edit and save one code/section at a time, and if it doesn't work to save the first time, retry.)
    No https encryption on web page.
    No TLS encryption for MQTT connection.
    TBD: Setting correct time zone from admin page.

#include <Arduino.h>
#include <LittleFS.h>
#include <ArduinoJson.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.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 working code for CodeLock 0.97 /hh with: 
    WiFi connectivity.
    Admin web page.
    MQTT connectivity for automations with Home Assistant or similar.
    Supports 8 multiple individual codes, consisting of the digits 0 through 9, 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, including timestamps, who unlocked the door, commands published from MQTT and admin page, and input from the keypad.
    Optional detection when the lock is in the unlocked state using a reed switch connected to D8. (Switch is assumed closed when the door is locked.)
    You can detect if the door is opened/closed instead, if there is trouble detecting the locked/unlocked state of your lock.
    Serial interface enabled in USB, 9600 baud.
*/
ESP8266WebServer 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 char* defaultDoorName = "Door";
const char* defaultAdminPassword = "adminpass";
const char* defaultCode = "12345678";
const int defaultValidFrom = 0;
const int defaultValidTo = 23;
const char* defaultRemark = "Default";
const int relayPin = D8; // GPIO15 (D8) // Using this pin does not activate relay during boot
const int reedSwitchPin = D0;  // GPIO15 (D8) Detects if handle is engaged
char topic[300];  // Ensure this buffer is large enough for your topic string
bool alwaysOpen = false;  // State variable
bool unLocked = true;     // State variable, always unlocked after a power on
bool lastState = HIGH;    // Track reed switch state, HIGH means unlocked
// bool currentState;
WiFiClient espClient;
PubSubClient client(espClient);
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", 3600, 60000);  // Timezone adjusted (+1h)

Code accessCodes[8];

String mqttServer = "";
String mqttUser = "";
String mqttPassword = "";
String mqttTopic = "";
// String topic = "CodeLock";
String adminPassword = defaultAdminPassword;
String doorName = defaultDoorName;
String message = "";

// 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<2048> 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;

  // 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<2048> 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>();
  
  // Load admin password
  adminPassword = doc["adminPassword"].as<String>();
  
  // Load door name
  doorName = doc["doorName"].as<String>();

  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 = timestamp + " Door unlocked by 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 /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.";
  } else {
    message = timestamp + " AlwaysOpen is now off.";
  }
      
  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 html = "<html><body>";
  html += "<h1>" + doorName + " CodeLock Configuration</h1>";
  
  // 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 += "}";

  // 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 += "<h3>Code " + String(i + 1) + "</h3>";
    html += "Code: <input type=\"text\" name=\"code" + String(i) + "\" value=\"" + accessCodes[i].code + "\" maxlength=\"8\"><br>";
    html += "Valid From (0-23): <input type=\"number\" name=\"validFrom" + String(i) + "\" value=\"" + String(accessCodes[i].validFrom) + "\" min=\"0\" max=\"23\"><br>";
    html += "Valid To (0-23): <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>";
  }

  // 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=\"text\" 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>";

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

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

  server.send(200, "text/html", html);
}


// Function to save configuration
void handleSave() {
  StaticJsonDocument<2048> 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");

  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 unlockDoor() {
  digitalWrite(relayPin, HIGH);  // Pull relay
  delay(500);                    // for 0.5 s.
  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 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 = timestamp + " Door unlocked via MQTT";
    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;
    String timestamp = getFormattedTime();
    message = timestamp + " AlwaysOpenOn set via MQTT";
    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 = timestamp + " AlwaysOpenOff set via MQTT";
    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() {
    if (client.connect("CodeLock", mqttUser.c_str(), mqttPassword.c_str())) {
      char sub_topic[300];  // Subscription topic buffer
      snprintf(sub_topic, sizeof(sub_topic), "%s/CodeLock/command", mqttTopic.c_str());
      client.subscribe(sub_topic);  // Subscribe using sub_topic
      snprintf(topic, sizeof(topic), "%s/CodeLock/activity", mqttTopic.c_str());
      client.publish(topic, "Connected to MQTT");
    }
}

// Kontrollera giltighetstid
bool isValidTime(int startHour, int endHour) {
  int currentHour = timeClient.getHours();  // Använder NTP-tid
  return currentHour >= startHour && currentHour <= endHour;
}

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
          String timestamp = getFormattedTime();
          message = timestamp + " Door unlocked by " + accessCodes[i].remark;
          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 = timestamp + " Code valid for " + accessCodes[i].remark + ", but not within allowed time.";
          Serial.println(message.c_str());
          snprintf(topic, sizeof(topic), "%s/CodeLock/activity", mqttTopic.c_str());
          client.publish(topic, message.c_str());
        }
      }
    } else {
      // 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(500);
  }

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

  pinMode(relayPin, OUTPUT);
  digitalWrite(relayPin, LOW);
  pinMode(reedSwitchPin, INPUT_PULLUP);


  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(".");
  }
  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());

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

  // Starta NTP-tjänsten
  timeClient.begin();

  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);
  // Start the server
  server.begin();

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

String getFormattedTime() {
  struct tm timeinfo;
  if (!getLocalTime(&timeinfo)) {
    return "TimeUnavailable";  // Handle case if time retrieval fails
  }
  char buffer[20];
  strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &timeinfo);
  return String(buffer);

}


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

  timeClient.update();  // Update time from NTP

  lastState = unLocked; // Save previous state to see if something changes just now.
  unLocked = digitalRead(reedSwitchPin); //Check if lock is unLocked

  if (unLocked == false && lastState == true) {
    // ReedSwitch changed state.
    // ReedSwitch indicates door handle has been pressed, 
    // when released, lock will be locked.
    String timestamp = getFormattedTime();
    message = timestamp + " Door handle pressed.";
    Serial.println(message.c_str());
    snprintf(topic, sizeof(topic), "%s/CodeLock/activity", mqttTopic.c_str());
    client.publish(topic, message.c_str());
    delay(1000);
    timestamp = getFormattedTime();
    message = "If closed, door will be locked";
    Serial.println(message.c_str());

    snprintf(topic, sizeof(topic), "%s/CodeLock/activity", mqttTopic.c_str());
    client.publish(topic, message.c_str());    
  }

  if (unLocked == true && lastState == false) {
    //ReedSwitch changed state, Door is now Unlocked
    // reedSwitch indicates door handle is engaged, but not yet pressed
    String timestamp = getFormattedTime();
    message = timestamp + " Door is now unlocked ";
    Serial.println(message.c_str());
    snprintf(topic, sizeof(topic), "%s/CodeLock/activity", mqttTopic.c_str());
    client.publish(topic, message.c_str());
  }

  if (alwaysOpen == true && unLocked == false) {  // Is this right?
    // alwaysOpen is set but door is locked, so unlock door again
    String timestamp = getFormattedTime();
    message = timestamp + " Door unlocked because AlwaysOpen is enabled. ";
    Serial.println(message.c_str());
    snprintf(topic, sizeof(topic), "%s/CodeLock/activity", mqttTopic.c_str());
    client.publish(topic, message.c_str());
    delay(500);
    unlockDoor();  // Call existing unlock function
  }
  lastState = unLocked;

  char key = keypad.getKey();
  if (key) {
    handleKeyInput(key);
  }
}