Aqua Angel- The Smart Aquarium Controller

Published Jun 03, 2020
 10 hours to build
 Intermediate

A web enabled aquarium controller using esp8266. Can control upto 8 aquarium accessories like Lights, filter Pump and heaters. Has scheduling features, sunrise/ sunset modes, even sunrise/sunset simulations. Configuration and device assignment is easy using a web interface. It is Alexa enabled and can control the devices using Alexa commands.

display image

Components Used

ESP8266 WiFi Module
ESP8266 is a system on chip (SoC) which provides WIFI capability for embedded applications. This enables internet connectivity to embedded applications. ESP8266 modules are mostly used in Internet of Things(IoT) applications.
1
Connecting Wire Jumper Wires
Connecting Wire Breadboard wires
1
Relay module 4 Channel
Relay module 4 Channel
1
5V SMPS
Switching Power Supplies AC-DC 35W LOW COST
1
Description

Introduction

I have a 106 litre Aquarium at home. The aquarium came with usual manual switches for the lights and pump. The aim of the project is to automate the operation of the equipment. The salient features planned are

  1. Web based interface for control & configuration
  2. Minimum hard coding and use of configurations so that code recompile is not required
  3. Use NTP for date & time requirements.
  4. Use Latlong to get sunrise/sunset times for lighting control.
  5. Alexa enabled to be abreast of technology

Design

1.Web based interface for control & configuration

The web interface is implemented.

a snippet of the code is given below.

As a mDNS is implemented, if the router supports it, this can be accessed at http://aquaangel.local:8080. Port 8080 has been used because port 80 is used by the espalexa component.

if (client) {                             // If a new client connects,
    Serial.println("New Client.");          // print a message out in the //Serial port
    String currentLine = "";                // make a String to hold incoming data from the client
    while (client.connected()) {            // loop while the client's connected
      if (client.available()) {             // if there's bytes to read from the client,
        char c = client.read();             // read a byte, then
        //Serial.write(c);                    // print it out the //Serial monitor
        header += c;
        if (c == '\n') {                    // if the byte is a newline character
          // if the current line is blank, you got two newline characters in a row.
          // that's the end of the client HTTP request, so send a response:
          if (currentLine.length() == 0) {
            // HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
            // and a content-type so the client knows what's coming, then a blank line:
            client.println("HTTP/1.1 200 OK");
            client.println("Content-type:text/html");
            client.println("Connection: close");
            client.println();

            xy = 0;
            //Serial.println(header);
            for (xy == 0; xy <= nopins; xy++)
            {
              char getstron[12] = "";
              char getstroff[12] = "";
              sprintf(getstron,  "GET /%i/on", equipment[xy].pin);
              sprintf(getstroff, "GET /%i/off", equipment[xy].pin);

              //Serial.println(getstroff);
              //Serial.println(getstron);
              if (header.indexOf(getstron) >= 0) {
                equipment[xy].switch_state = 1;
                equipment[xy].state = 1;
                digitalWrite(equipment[xy].pin, LOW);
                Serial.println(digitalRead(equipment[xy].pin));
              }
              if (header.indexOf(getstroff) >= 0) {
                equipment[xy].switch_state = 0;
                equipment[xy].state = 0;
                digitalWrite(equipment[xy].pin, HIGH);
                Serial.println(digitalRead(equipment[xy].pin));
              }

            }
            if (header.indexOf("saveconfig") >= 0) {
              Serial.println("Entering save Configuration page");
              File f = SPIFFS.open("config.txt", "w");

              if (!f) {
                Serial.println("file open failed");
              }
              else
              {
                Serial.println("File opened");
              }
              //Serial.println(header);
              Serial.println("____");
              int n = header.length();
              char savestr[n + 1];
              memset(savestr, '\0', sizeof(savestr));
              char *ret;


              strcpy(savestr, header.c_str());
              decode(header.c_str(), savestr);
              Serial.println(savestr);
              //char* token1 = strtok(savestr, "HTTP");
              //Serial.println(token1);
              Serial.println("HTTP at");
              Serial.println(isSubstring(savestr, "HTT"));
              char savestr2[isSubstring(savestr, "HTT") + 1];
              memset(savestr2, '\0', sizeof(savestr2));
              Serial.println("memset");
              strncpy(savestr2, savestr, isSubstring(savestr, "HTT"));
              //Serial.println(savestr2);
              char* token = strtok(savestr2, "&");
              while (token != NULL) {
                //Serial.println(token);

                ret = strchr(token, '=');
                ret++;
                //Serial.println(ret);
                f.print(ret);
                f.print(",");
                token = strtok(NULL, "&");
              }
              f.close();
              Serial.println("File Closed");
              ESP.restart();
            }


            // Display the HTML web page
            client.println("<!DOCTYPE html><html>");
            client.println("<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
            client.println("<link rel=\"icon\" href=\"data:,\">");
            // CSS to style the on/off buttons
            // Feel free to change the background-color and font-size attributes to fit your preferences
            client.println("<style>html { font-family: Helvetica; font-size:20pt;display: inline-block; margin: 0px auto; text-align: center;}");
            client.println(".button1 { background-color: Green;color:Yellow;border-radius: 10%; font-size:17pt;width:60pt;align:center;}");
            client.println(".button2 {background-color:Red;border-radius: 10%;font-size:17pt;width:60pt;align:center;}");
            client.println(".switch {position: relative;display: inline-block;width: 60px;height: 34px;}");
            client.println(".switch input {opacity: 0;width: 0;height: 0;}");
            client.println(".slider {position: absolute;cursor: pointer;top: 0;left: 0;right: 0;bottom: 0;background-color: #ccc;-webkit-transition: .4s;transition: .4s;}");
            client.println(".slider:before {position: absolute;content: "";height: 26px;width: 26px;left: 4px;bottom: 4px;background-color: white;-webkit-transition: .4s;transition: .4s;}");

            client.println("input:checked + .slider {background-color: #2196F3;}");
            client.println("input:focus + .slider {box-shadow: 0 0 1px #2196F3;}");
            client.println("input:checked + .slider:before {-webkit-transform: translateX(26px);-ms-transform: translateX(26px);transform: translateX(26px);}");
            client.println(".slider.round {border-radius: 14pt;}");
            client.println(".slider.round:before {border-radius: 50%;}");
            client.println("TD {border-bottom: 3px solid black;border-right: 3px solid black;background-color:#42f4f1;}");
            client.println("TH {border-bottom: 3px solid black;border-right: 3px solid black;background-color:blue;color:yellow;}</style></head>");



            // Web Page Heading
            client.println("<body>Aqua Angel<BR>");
            if (mdnsflag == 1)
            {
              client.println("mDNS setup<BR>");
            }
            //Serial.println(crntime);
            client.println(crntime);

            //client.print(currentTime);
            client.println("<table align='center' cellpadding='0' cellspacing='0' style='border-left:3pt solid black;border-top:3pt solid black;background-color:#42f4f1;'>");
            client.println("<TR><TH>Equipment</TH><TH>State</TH><TH>Start</TH><TH>End</TH></TR>");
            xy = 0;
            for (xy == 0; xy <= nopins; xy++)
            {
              if (strcmp(equipment[xy].ename, "Dummy") != 0)
              {
                client.println("<TR><TD>");
                client.println(equipment[xy].ename);
                //client.println(equipment[xy].current_state);
                client.println("</TD>");
                //if (equipment[xy].current_state == 1) {
                if (digitalRead(equipment[xy].pin) == LOW) {
                  client.println("<TD><a href=\"/");
                  client.println(equipment[xy].pin);
                  client.println("/off\"><button class=\"button2\"> ON</button></a></TD>");
                  //client.println("/off\"><label class='switch'><input type='checkbox' checked><span class='slider round'></span></label></a></TD>");

                } else {
                  client.println("<TD><a href=\"/");
                  client.println(equipment[xy].pin);
                  client.println("/on\"><button class=\"button1\">OFF</button></a></TD>");
                  //client.println("/on\"><label class=\"switch\"><input type=\"checkbox\" ><span class=\"slider round\"></span></label></a></TD>");
                }
                client.println("<TD>");
                //String displaytime = String(int(equipment[xy].start_time / 60)) + ":" + String(int(equipment[xy].start_time % 60));
                sprintf(displaytime, "%02d:%02d", int(equipment[xy].start_time / 60), int(equipment[xy].start_time % 60));
                client.println(displaytime);
                //client.println(equipment[xy].start_time);
                client.println("</TD>");
                client.println("<TD>");
                //String displayetime = String(int(equipment[xy].end_time / 60)) + ":" + String(int(equipment[xy].end_time % 60));
                sprintf(displaytime, "%02d:%02d", int(equipment[xy].end_time / 60), int(equipment[xy].end_time % 60));
                client.println(displaytime);
                //client.println(equipment[xy].end_time);
                client.println("</TD>");
                client.println("</TR>");
              }
            }
            client.println("</table>");
            Serial.println("Page displayed..");
            client.println("<button onclick='myFunction()' style='font-size:10pt;background-color:lightgrey;'>Configure</button>");
            client.println("<font size='1'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Running since:");
            client.println(starttime);
            client.println("<font size='1'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Input Voltage:");
            float vcc = ESP.getVcc() / 1000.00;
            client.println(vcc, 3);
            //Serial.println(vcc, 3);
            client.println("</font>");
            client.println("<div id='myDIV' style='display:none;'>");
            client.println("<table align='center' cellpadding='0' cellspacing='0' style='font-size:10pt;border-left:3pt solid black;border-top:3pt solid black;background-color:#42f4f1;'>");
            client.println("<TH>Equipment</TH><TH>Pin</TH><TH>Initial State</TH><TH>Trigger</TH><TH>Start Time</TH><TH>End Time</TH>");
            int incount = 0;

            client.println("<FORM name='config' action='/saveconfig' method='GET'>");
            //client.println(savestr);
            // client.println("' method='POST'>");

            incount = 0;
            for (incount == 0; incount <= nopins; incount++)
            {
              client.print("<TR><TD><input type='text' name='equip_");
              client.print(incount);
              client.print("' value='");
              client.print(equipment[incount].ename);
              client.print("' maxlength='25'></TD>");
              client.print("<TD><input type='text' name='pin_");
              client.print(incount);
              client.print("' value='");
              client.print(equipment[incount].pin);
              client.print("' maxlength='1' max='16' style='width:30px;' readonly></TD>");
              client.print("<TD><input type='number' name='i_state_");
              client.print(incount);
              client.print("' value='");
              client.print(equipment[incount].initial_state);
              client.print("' maxlength='1' style='width:30px;'></TD>");
              client.print("<TD><input type='text' name='trigger_");
              client.print(incount);
              client.print("' value='");
              client.print(equipment[incount].trigger);
              client.print("' maxlength='1' pattern='[NSMTC]' style='width:30px;'></TD>");
              client.print("<TD><input type='text' name='starttime_");
              client.print(incount);
              client.print("' value='");
              client.print(equipment[incount].start_time);
              client.print("' maxlength='5' style='width:40px;'></TD>");
              client.print("<TD><input type='text' name='endtime_");
              client.print(incount);
              client.print("' value='");
              client.print(equipment[incount].end_time);
              client.print("' maxlength='5' style='width:40px;'></TD>");
              client.println("</TR>");
            }
            client.println("<input font-size='6' type='submit' value='Save Configuration'>");
            client.println("</FORM></table>");
            client.println("</div>");
            client.println("<script>");
            client.println("function myFunction() {");
            client.println("var x = document.getElementById('myDIV'); ");
            client.println("if (x.style.display == 'none') {");
            client.println("x.style.display = 'block'; ");
            client.println("} else {");
            client.println("x.style.display = 'none'; ");
            client.println(" }");
            client.println("}");
            client.println(" </script> ");
            client.println("</body> </html> ");

            // The HTTP response ends with another blank line
            client.println();
            // Break out of the while loop
            break;
          } else { // if you got a newline, then clear currentLine
            currentLine = "";
          }
        } else if (c != '\r') {  // if you got anything else but a carriage return character,
          currentLine += c;      // add it to the end of the currentLine
        }
      }
    }
    // Clear the header variable
    header = "";
    // Close the connection
    client.flush();
    client.stop();
    //ESP.restart();
    //Serial.println("Client disconnected.");
    //Serial.println("");
  }

The initial configurations are store in a struct.

struct {
 char ename[50];
 int pin;
 int initial_state;
 int switch_state;
 char trigger;
 int state;
 int start_time;
 int end_time;
} equipment[] = {
 {"Filter Pump", 16, 0, 0, 'N' , 0, 0, 0 },
 {"Air Pump", 5, 1, 0, 'S', 0, 0 , 0},
 {"Day light", 4, 1, 0, 'S', 0 , 0, 0},
 {"Moon light", 0, 1, 0, 'M', 0, 0, 0},
 {"Dummy", 2, 0, 0, 'M', 0 , 0, 0},
 {"Dummy", 14, 0, 0, 'T', 0, 1230, 1320},
 {"Dummy", 12, 0, 0, 'T', 0, 1230, 1320},
 {"Dummy", 13, 0, 0, 'T', 0, 1230, 1320},
 {"Dummy", 15, 0, 0, 'T', 0, 1230, 1320},
};

The devices connected to the esp8266 GPIOs can be configured using the configuration page.

The configuration is then stored in SPIFFS. These are loaded when the ESP boots.

2. Network Configuration.

 The project used the WiFiManager library to assist the network configuration. 

 

The Latitude and Longitude and timezone are configured here and saved. These are required for sunrise/sunset modes.

3. Time 

The TimeLord library is used to obtain the sunrise/sunset times for the day.

Instead of using a RTC module the project uses NTP to get the current time.

The NTPtimeESP library is used for obtaining time from NTP server time.google.com

 

4. Alexa Enablement

Alexa voice control has become very popular for voice control of devices. This is achieved using the espalexa library.

xy = 0;
  for (xy == 0; xy <= nopins; xy++)
  {
   
    pinMode(equipment[xy].pin, OUTPUT);
    digitalWrite(equipment[xy].pin, equipment[xy].initial_state);
    if ( (strcmp(equipment[xy].ename, "Dummy") != 0) ) {
      espalexa.addDevice(equipment[xy].ename, onSetState, EspalexaDeviceType::onoff);
     
      Serial.print("added to Alexa ");
      Serial.println(equipment[xy].ename);

    }
  }
  espalexa.begin();
}

void onSetState(EspalexaDevice* d) {
  Serial.print(d->getId());
  Serial.println(d->getValue());
  Serial.print(equipment[d->getId()].ename);
  Serial.println(" switched by Alexa");

  if (d->getValue() == 0) {
    digitalWrite(equipment[d->getId()].pin, LOW);
    equipment[d->getId()].switch_state = 1;
    equipment[d->getId()].state = 1;
  } else {
    digitalWrite(equipment[d->getId()].pin, HIGH);
    equipment[d->getId()].switch_state = 0;
    equipment[d->getId()].state = 0;
  }
}

  On asking Alexa to search for devices, the devices will be discovered by Alexa and will be added as Wemo switches. You can then control by saying " Alexa switch on the filter pump" / "Alexa switch off the filter pump". 

 

5.Program Updations:

 The project has Over The Air update (OTA) feature. This is useful if you want to update the firmware after it has been put into an enclosure.

The ArduinoOTA library has been used.

/***************************************
    Code for OTA
  ***************************************/
 ArduinoOTA.onStart([]() {
   //Serial.println("Started OTA");
 });
 ArduinoOTA.onEnd([]() {
   //Serial.println("\nEnded OTA");
 });
 ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
   //Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
 });
 ArduinoOTA.onError([](ota_error_t error) {
   //Serial.printf("Error[%u]: ", error);
   if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
   else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
   else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
   else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
   else if (error == OTA_END_ERROR) Serial.println("End Failed");
 });
 ArduinoOTA.begin();

6. Compiling the code

The options to be selected while compiling the source code is shown below

 

7. Connecting it all together.

Project wiring diagram

Tne Aquarium controller before boxup

Before boxup

The Aquarium Controller after boxup

Demo of the Aquaangel- The Smart Aquarium Controller

Video

Demo of the network Configuration

Demo of Web Interface

 

 

Codes

Downloads

aqua Download
Comments
Ad