Zeven Development

Garage Hydroponics (Using New IO Adder!)

Build your own Professional Hydroponics Control System in your garage!


Grow your own produce 365 days a year at 40% higher yields than soil.

Growbed

After 4 weeks, they are almost ready to harvest!

Harvest

First find a nice empty space in the garage that gives you access to all sides of your hydroponics system.


Fans

Most garages are not heated and cooled, so to help maintain a somewhat more stable environment, insulation is your friend. The most obvious and mandatory part of the garage is to insulate first is the garage door. Use insulation material that you can get at your local hardware store. I chose Rmax R-Matte Plus-3 3/4", 4ft x 8ft sheets that I cut to size using an exacto knife. The fitted sizes were then cut in half horizontally and compressed fit into the garage door slots making sure the foil side was facing the outside with a half inch air gap. This air gap gave me the equivalent of a total R factor of 6. The better the insulation, the less money you will be paying heating and cooling later.


Fans

Garage Parts


Qty Part Description Cost
6 Rmax R-Matte Plus-3 3/4" x 4' x 8'. R-5 Polyisocyanurate Rigid Foam Insulation Board $15.53

Note: Make sure that you seal all the areas in your garage that would allow the outdoor air to get in.


Since you will be heating, cooling, and supplying artificial plant light it is recommended to add a dedicated breaker for your hydroponics system. Get a licensed electrician to add you a new 20 amp GFI breaker. Most breakers are in the garage so adding a new circuit should be a relatively low cost option for better isolation and safety.


Fans

Build your hydroponic tent in the empty space in your garage.


Fans

Hydroponic Tent Parts


Qty Part Description Cost
1 VIVOSUN 96"x48"x80" Mylar Hydroponic Grow Tent Room $195.99
1 VIVOSUN 8" Inline Duct Fan w/ Speeder Air Carbon Filter Ducting Combo $117.99
1 Quantum Storage 4-Shelf Wire Shelving Unit, 300 lb. Load Capacity/Shelf. 72"H x 48"W x 24"D. $111.77
4 Durolux DLED8048W 320W LED Grow Light. 4' x 1.5' 200W, White FullSun. $90.00
1 VIVOSUN 6" 2-Speed Clip On Oscillating Fan. $31.99
1 Pelonis Electric Oil Filled Heater with Adjustable Thermostat Black. $47.79
1 AC/DC 5V-400V Snubber Board Relay Contact Protection Absorption Circuit Module. $1.38

Shelf/Light Setup


Even though the shelf comes with four levels I only used three so that I would have enough light and grow room. The shelf can be custom configured to any number of shelves and height, with the ability to hold up to 300 lbs per shelf. On the right side of the tent I added a grow light to accommodate larger plants. I personally prefer the white LED lights, but you can use any high quality hydroponic grow light you prefer. LED lights are preferred because they are lower powered with less heat generated, and longer lasting. The Durolux DLED8048W only uses 200W with a CCT of 5946K full sun spectrum.


Fans

Carbon Filter/Duct Setup


On the right side of the hydroponics tent I installed the carbon filter and vent fan to duct the air out towards the garage door. If desired you can also duct this air outside if your garage is smaller. Circulating fresh air is critical in maintaining healthy plants.


Fans

Hydroponics Method


For this hydroponics systems we will be using the Aeroponics method. This is the most advanced of the hydroponic methods that we will be highly automating so that you will be able to just 'watch and grow' your crop. This method allows for the highest accelerated growth rate and crop yields. For this method the plant roots will be always submerged in oxygen rich aerated reservoir of water. This also makes it the most complex and difficult to setup of the different hydroponic methods. But no worries, with the proper control systems it will easy to manage and maintain.


Aeroponics Parts


Qty Part Description Cost
1 VIVOSUN Air Pump 950 GPH 32W 60L/min 6 Outlet. $63.95
1 UDP 10' 1/4" ID x 7/16" Clear Braided Vinyl Tubing. $6.96
1 UDP 10' 1/2" ID x 3/4" OD Clear Braided Vinyl Tubing. $12.64
2 0.170" ID x 1/4" OD 20ft Clear Vinyl Tubing. $3.92
1 Pawfly 5 Pcs Non-Return Oxygen Air Pump Regulator Check Valve. $4.99
1 10 Pcs 2 Way Clear Elbow Aquarium Air Connector. $3.56
2 12" Air Stone Bubble Curtain Bar. $8.98
2 Sterilite 10 gal. Tote Black 25-3/4" x 18-1/4" x 7" h. $12.99
1 Sterilite 4 gal. Tote Black 18" x 12-1/2" x 7" h. $8.99
1 x25 Black 3" Net Pots Cups - Heavy Duty NO PULL THRU Rim Design. $9.34
1 10 Liters HYDROTON Clay Pebbles Growing Media Expanded Clay Rocks. $28.23
1 VIVOSUN 6 Mil Mylar Film Roll 4' x 10' Diamond Film Foil Roll. $20.99

Aeroponics Setup


We are going to setup two aerated water resevoirs, each with nine planters. First create a cardboard template that is 13.5" x 5.5" and drill guide holes at the center of 6.75" and 4.25" out on either side from the center hole.

Template

Using this guide drill nine holes in the lid of the 10 gallon resevoir tote.


ToteTemplate

Then using a 3" drill bit, reverse drill out the 3" holes for the net cups.


ToteDrill

After you have drilled the nine holes, make sure that the net cups fit easily and flush into the holes.  Sand if necessary.


ToteNetCups

Black totes have been chosen for a very specific reason; it does not allow any light to enter the resevoir, thus reducing algae growth, but black does absorb the light overhead and can increase the water temperature inside. To mitigate this we will use the mylar film and create a folded cover with the same hole cut outs as on the tote lid to reflect this light.

Cut a rectangle sheet of 40" x 32.5" of mylar and fold in 6.5" on all the sides, and crease them.  Position and center the tote lid on the underside of the mylar so it fits evenly with in all the edges and using the tote lid as a template, use a marker to draw the cut out circles.  Cut along the outside edge of the marked circles to create your nine net cup holes. Finally use your origami skills and fold in the corners and staple them in place.


ToteCover

Then insert the cover over the lid and fill your net cups with small clay rocks.


ToteTop

Or if you want to grow larger leaf plants then create a six planter water resevoir. Create a cardboard template that is 13.5" x 5.5" and drill guide holes at 4" and 10".

TemplateSix

Using this guide, drill six holes in the lid of the 10 gallon resevoir tote.

ToteDrillSix

Using the same process on the myar, cut out six net cup holes, fold and assemble.

ToteTopSix

Secure the air pump to the bottom of the 4 gallon tote and drill and secure the appropriate air hoses for an in and out air flow with tie wraps. If you can buy the hose by the foot at your local hardware store, it will be cheaper.  Secure the lid on the tote. We are concealing the air pump to help reduce the noise generated by the air pump. The air pump generates heat.  So, during winter place the air pump tote in the hydroponics tent to help heat it and during the summer outside to help reduce the heat. The air intake hose should always pull air from the outside of the hydroponics tent to get fresh air.


TotePump

To further reduce the noise attach 3/4" self adhesive pipe insulation on the under sides of the tote, and place a heavy object on the top of the tote

ToteNoise

Using the cut notch in the back of the reservoir tote for the chiller, connect the air hose to the elbow connecter, flow valve, and finally to the air stone. The elbow will rest in the notch preventing the air hose from kinking, and the check flow value will stop any water in the reservoir from back flowing into the air pump during a power loss.  Make sure you get barbed check flow values, or the pressure from the air pump will keep pushing the hose off. Place the 12" air stone long ways in the center bottom of the tote between the chiller coil.


Note: Use a small application of olive oil to make it much easier to slip the air hose onto the connectors.


AirHoseInstall

Hydroponics Control System


The Hydroponics Control System is actually a combination and addition of two earlier projects.



The IO Expander was designed for Hydroponics/Aquaponics Systems where extreme sensor IO is needed, as you will see when all the accessory parts finally come together.


Final

Feature List

  • Inside/Outside Temperature/Humidity Sensor.
  • Smart vent fan control with absolute humidity comparison.
  • Smart ventilating saves power.
  • Lighting controls.
  • Automatic Temperature control.
  • Battery Backed Real Time Clock for Scheduling.
  • Non Volatile storage. Backup current states.
  • Smart power control and monitoring.
  • WiFi Connectivity.
  • WiFi logging of real time data.
  • WiFi alerting to your smart phone.

Hydroponics Control System Parts


Qty Part Description Cost
1 IO Expander. $35.00
1 IO Expander Bundle. $35.00
1 BMOUO 12V 30A DC Universal Regulated Switching Power Supply 360W. $18.98
1 NodeMcu ESP8266 ESP-12E Wireless WiFi board. $4.75
1 12V 16-Channel Relay Module. $13.99
1 DS3231 AT24C32 I2C Precision Real Time Clock Memory Module. $4.95
2 FS200-SHT10 Soil Temperature and Humidity Sensor Probe. $20.79
2 1 Port Surface Mount Box White. $0.52
2 1.3" I2C 128x64 SSD1306 OLED LCD Display White. $8.65
1 4 Pcs Dual Row 8 Position Screw Terminal Strip 600V 25A. $14.61
1 7 Terminal Ground Bar Kit. $5.98
1 265x185x95mm Waterproof Clear Electronic Project Box Enclosure Plastic Case. $40.00

Wiring Diagram

Wiring

Note: Where you see the 'X' in the phone cable indicates a reverse wiring.


OLED Display

OLED

Note: Humidity outlined is below minimum and temperature inverted is above maximum warning.


So why use the IO Expander?


  • Simpler to Design.
  • Off-The-Shelf Parts.
  • No 1-Wire Driver to Write.
  • No Relay Driver to Write.
  • No OLED Display Driver to Write.
  • No Display Fonts to Take ESP8266 Code Space.
  • No Humidity Sensor Driver to Write.
  • No DS3231 RTC Driver to Write.
  • No AT24C32 EEPROM Driver to Write.
  • Saves Code Space on ESP8266.
  • Easy to Wire Using Standard RJ11 Phone Cable.
  • No Sensor Cable Length Issues.
  • Cheaper to Build Than Commercial Systems.
  • Easy to Make Changes to Adapt to Individual Requirements.
  • Single Power Supply.

Hydroponics Control System


Drill holes in the bottom of the project case and secure the power terminals. The left side is for 110VAC, and the right side is for 12VDC. Drill holes and install gland nuts for 110VAC, 12VDC, and data line inputs/outputs on the bottom side of the project case.


Terminal

Warning: Only perform this if you are comfortable working with high power voltages!

Danger

Wire in the 110VAC power lines needed and connect the live wire (black) to the lower relays.


Power

Run and connect the 12V relay power lines needed to the upper relays.


Power12v

Once all the power lines are run make sure you place the protector covers over the terminal blocks to prevent any accidental contact.

Place a thin layer of insulation foam under the relay board and over the 12VDC power terminal, so it is fully insulated.


Insulate

Note: When installing the wires into the terminal blocks and relay screw terminals it is helpful to first tin the wires with solder.

When connecting the 1-Wire to I2C to the DS3231 and then to the two SSD1306 OLED screen you will have a total of four different pullups on the SDA and SCL lines as shown in the image below circled in yellow. This will effectively result in a 4.7k / 4 = 1.175k pullup that will be too strong for the I2C bus to operate properly.

I2CPullups

Since the DS3231 uses a resistor pack that is used by other lines remove the other pullup resistors:

  • 1-Wire to I2C R3 and R4.
  • Both SSD1306 OLED R6 and R7.
  • Move the 4.7k pullup circled in green on the second OLED screen from the address select 0x78 to 0x7A.

Note: Depending upon the type of 1.3" OLED display you get, the resistor shown may not be the same.

In order to connect the growbed module ports 1 and 2 need to be converted to 1-wire® overdrive ports by adding 2.2K pullups. This can be done easily by soldering an 0603 2.2K resistor between the pins on the underside of the IO Expander.


GrowbedPullups

Finally assemble all the boards to complete the Hydroponics Control System. Additional standoffs can be drilled and added to secure the relay and IO Expander board. Use two sided tape to secure smaller boards.


Assembly

ESP8266 Code (OTA)


Make the necessary changes to the following code to specify your WiFi router SSID, Password, and sensor addresses marked by '** Change **'. Then program your ESP8266 NodeMCU using the USB port one time only. Future updates can now be made over the air (OTA) so you can now keep your project box closed and still make updates.


/* IO Expander

   Garage Hydroponics System v2.0

*/


#include <math.h>
#include <time.h>
#include <stdlib.h> /* qsort */
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <ESP8266HTTPClient.h>
#endif
#if defined(ARDUINO_ARCH_ESP32)
#include <WiFi.h>
#include <HTTPClient.h>
#endif
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
#include <NTPClient.h>
#include <ArduinoJson.h>
#include "IOExpander.h"

#ifndef SSID
#define SSID "RouterName"  // *** Change RouterName
#define PSK  "RouterPassword" // *** Change RouterPassword
#define HOST "http://www.mywebsite.com" // *** Change mywebsite.com
#define MySQL
#define MSSQL
#ifdef MySQL
#define MYSQL_URL "http://192.168.1.50/hydroponics/adddata.php" // *** Change 192.168.1.50
const char* mysql_url = MYSQL_URL;
#endif
#ifdef MSSQL
#define MSSQL_URL "http://www.zevendevelopment.com/hydroponics/adddata.aspx" // *** Change mywebsite.com
const char* mssql_url = MSSQL_URL;
#endif
#endif

#define TZ_POSIX                "EST+5EDT,M3.2.0/2,M11.1.0/2"

WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP); //, EST_OFFSET);
long tzoffset;

const char* ssid = SSID;
const char* password = PSK;
const char* host = HOST;

#define LED_BUILTIN             2

#define SerialDebug             Serial1     // Debug goes out on GPIO02
#define SerialExpander          Serial      // IO Expander connected to the ESP UART

#define FAHRENHEIT
#define ONEWIRE_TO_I2C_MAIN     "i4s08"     // *** Change 08
#define RTC_SENSOR              "s4te"
#define I2C_EEPROM              "s4tf"
#define INIT_OLED1              "st13;si;sc;sd"
#define INIT_OLED2              "st133d;si;sc;sd"
//#define HUMIDITY_SENSOR_INSIDE  "s6t5"      // DHT22
//#define HUMIDITY_SENSOR_OUTSIDE "s8t1"      // SHT10
// Free port 5-8 by using a splitter on port 3 and use I2C SHT3x humidity sensors
#define HUMIDITY_SENSOR_INSIDE  "i3s5a;ic0;st3" // SHT3x 100kHz w/ 2.2k pullup *** Change 5a
#define HUMIDITY_SENSOR_OUTSIDE "i3s0e;ic0;st3" // SHT31 100kHz w/ 2.2k pullup *** Change 0e
#define ALL_RELAYS_OFF          "esffff"
#define VENT_FAN_ON             "e1o"
#define VENT_FAN_OFF            "e1f"
#define LIGHTS_ON               "e2o"
#define LIGHTS_OFF              "e2f"
#define HEATER_ON               "e3o"
#define HEATER_OFF              "e3f"
#define CHILLER_ON              "e4o"
#define CHILLER_OFF             "e4f"
#define WATER_PUMP_ON           "e5o"
#define WATER_PUMP_OFF          "e5f"
#define HEATER_PAD_ON           "e6o"
#define HEATER_PAD_OFF          "e6f"

#define SEC_IN_MIN              60
#define MIN_IN_HOUR             60
#define HOURS_IN_DAY            24
#define MIN_IN_DAY              (MIN_IN_HOUR * HOURS_IN_DAY)
#define DAYS_IN_WEEK            7

#define ROOM_VOLUME             (96*48*80)  // Grow room Length * Width * Height in inches
#define FOOT_CUBE               (12*12*12)  // Convert inches to feet volume
#define VENT_FAN_CFM            720         // Cubic Feet per Minute
#define VENT_FAN_POWER          190         // Fan power in Watts
#define DUCT_LENGTH             2           // Short=2, Long=3
#define AIR_EXCHANGE_TIME       5           // Exchange air time.  Every 5 minutes
#define VENT_FAN_ON_TIME        ((((ROOM_VOLUME*DUCT_LENGTH)/FOOT_CUBE)/VENT_FAN_CFM)+1)

uint8_t OVERRIDE_VENT_FAN;
uint16_t OVERRIDE_VENT_FAN_TIME = 0;

#define MIN_DAY_TEMP            70          // Warm season crops daytime (70-80)
#define MAX_DAY_TEMP            80
#define MAX_OFF_TEMP            90          // Max temp to turn lights off
#define HEATER_ON_DAY_TEMP      66.5    
#define HEATER_OFF_DAY_TEMP     68.5
#define MIN_NIGHT_TEMP          60          // Nighttime (60-70)
#define MAX_NIGHT_TEMP          70
#define HEATER_ON_NIGHT_TEMP    66
#define HEATER_OFF_NIGHT_TEMP   64
#define MIN_HUMIDITY            50          // Relative humidity. Best=60%
#define MAX_HUMIDITY            70

#define MIN_WATER_TEMP          66          // 68F or 20C
#define MAX_WATER_TEMP          70            
#define SOLENOID_ON_WATER_TEMP  68.25        
#define SOLENOID_OFF_WATER_TEMP 67.75
#define CHILLER_ON_WATER_TEMP   45 //55
#define CHILLER_OFF_WATER_TEMP  40 //45
#define CHILLER_CYCLE_TIME      10          // Chiller minimum on/off time to protect compressor
#define CHILLER_RECOVERY_TIME   240         // Chiller recovery time needs to occur in this time

#define GERMINATION_ON_TEMP     74.5        // Germination heater pad temperature
#define GERMINATION_OFF_TEMP    75.5

#define LIGHTS_ON_HOUR          6           // Lights on from 6:00AM - 6:00PM (12 hrs)
#define LIGHTS_ON_MIN           0
#define LIGHTS_OFF_HOUR         18
#define LIGHTS_OFF_MIN          0
#define LIGHTS_POWER            (192*2)     // 4 Grow lights
#define LIGHTS_ON_DAY_MIN       ((LIGHTS_ON_HOUR * MIN_IN_HOUR) + LIGHTS_ON_MIN)
#define LIGHTS_OFF_DAY_MIN      ((LIGHTS_OFF_HOUR * MIN_IN_HOUR) + LIGHTS_OFF_MIN)

uint8_t OVERRIDE_LIGHTS;
uint16_t OVERRIDE_LIGHTS_TIME   = 0;

#define IOEXPANDER_POWER        3           // IO Expander, NodeMCU, x16 Relay, etc power in Watts
#define AIR_PUMP_POWER          32          // Air Pump power in Watts
#define CIRCULATING_FAN_POWER   20          // Circulating fan in Watts
#define HEATER_POWER            560         // Radiator heater in tent
#define ALWAYS_ON_POWER         (IOEXPANDER_POWER + AIR_PUMP_POWER + CIRCULATING_FAN_POWER)
#define DOSING_PUMP_POWER       8           // Peristaltic Dosing Pump 7.5W
#define CHILLER_SOLENOID_POWER  5           // Water Solenoid Valve 4.8W
#define CHILLER_POWER           121         // Freezer 5ct
#define WATER_PUMP_POWER        30          // Peristaltic Chiller Pump 1.4A * 12V = 16.8W
#define HEATER_PAD_POWER        20          // Germination Heat Pad in Watts

#define COST_KWH                9.8450      // First 1000 kWh/month
//#define COST_KWH                10.0527     // Over 1000 kWh/month

#define SERIAL_DEBUG
#define SERIAL_TIMEOUT          5000        // 5 sec delay between DHT22 reads

//#define MAX_SELECT_ROM          21
#define ERROR_NO_ROM            -1
#define ERROR_OVER_SATURATED    -2
#define ERROR_READ              -3

#define CO2_SAMPLES_IN_MIN      5
#define CO2_INTERVAL            (SEC_IN_MIN / CO2_SAMPLES_IN_MIN)
#define MAX_CO2_FAILS           10

#define NUTRIENT_MIX_TIME       2           // 2 minutes nutrient mix time.
#define MAX_WATER_PUMP_TIME     5           // 5 minutes of watering then give up

typedef struct {
  uint32_t energy_usage[DAYS_IN_WEEK];
  uint16_t energy_time[DAYS_IN_WEEK];
  uint8_t energy_wday;
  //uint8_t state;
  uint8_t crc;
} NVRAM;

struct HS {
  float temp;
  float relative;
  float absolute;
  bool error;
};

#define ONEWIRE_TEMP            "t2s0;tt;t1s0;tt"   // DS18B20 on pins 2 and 1 on all grow beds, chiller, and germination

const char ONEWIRE_TO_I2C_GROW1[] = "i2s36"; // IO Adder w/ I2C Bus - OLED Screen/Light Sensor *** Change 36
const char ONEWIRE_TO_I2C_GROW2[] = "i2sfb"; // IO Adder w/ I2C Bus - OLED Screen/Light Sensor *** Change fb
const char ONEWIRE_TO_I2C_GROW3[] = "i2sde"; // RJ11 Keystone Crossover Out, T-Connector w/ I2C Bus - OLED Screen/Light Sensor *** Change de

const char TEMP1_SENSOR[] =     "t2r92";    // RJ11 Keystone Crossover In for 1-Wire Junction DS18B20 *** Change 92
const char LEVEL1_SELECT[] =    "i2s36;st1a38"; // IO Adder *** Change 36
const char LEVEL1_SENSOR[] =    "sr6";      // IO Adder Optical Connector
const char TDS1_SELECT[] =      "i2s36;st1b"; // IO Adder *** Change 36
const char TDS1_SENSOR[] =      "sr0";      // IO Adder ADC
#define TDS1_CALIBRATION        (488.0/488.0) // TDS Calibration (Desired/Actual) *** Change
#define WATER1_RELAY            9           // Relay Water Dosing Pump
#define NUTRIENT1_RELAY         9           // Relay Nutrient Dosing Pump
#define CHILLER1_RELAY          15          // Relay Chiller Solenoid

const char TEMP2_SENSOR[] =     "t1r3f";    // RJ11 Keystone Crossover In for 1-Wire Junction DS18B20 *** Change 3f
#define LEVEL2_SELECT           LEVEL1_SELECT
const char LEVEL2_SENSOR[] =    "sr7";      // IO Adder Optical Connector
#define TDS2_SELECT             TDS1_SELECT
const char TDS2_SENSOR[] =      "sr1";      // IO Adder ADC
#define TDS2_CALIBRATION        (488.0/488.0) // TDS Calibration (Desired/Actual) *** Change
#define WATER2_RELAY            10          // Relay Water Dosing Pump
#define NUTRIENT2_RELAY         10          // Relay Nutrient Dosing Pump
#define CHILLER2_RELAY          16          // Relay Chiller Solenoid

const char TEMP3_SENSOR[] =     "t2r5b";    // RJ11 Keystone Crossover In for 1-Wire Junction DS18B20 *** Change 5b
const char LEVEL3_SELECT[] =    "i2sfb;st1a38"; // IO Adder *** Change fb
const char LEVEL3_SENSOR[] =    "sr6";      // IO Adder Optical Connector
const char TDS3_SELECT[] =      "i2sfb;st1b"; // IO Adder *** Change fb
const char TDS3_SENSOR[] =      "sr0";      // IO Adder ADC
#define TDS3_CALIBRATION        (488.0/488.0) // TDS Calibration (Desired/Actual) *** Change
#define WATER3_RELAY            11          // Relay Water Dosing Pump
#define NUTRIENT3_RELAY         11          // Relay Nutrient Dosing Pump
#define CHILLER3_RELAY          13          // Relay Chiller Solenoid

const char TEMP4_SENSOR[] =     "t1r24";    // RJ11 Keystone Crossover In for 1-Wire Junction DS18B20 *** Change 24
#define LEVEL4_SELECT           LEVEL3_SELECT
const char LEVEL4_SENSOR[] =    "sr7";      // IO Adder Optical Connector
#define TDS4_SELECT             TDS3_SELECT
const char TDS4_SENSOR[] =      "sr1";      // IO Adder ADC
#define TDS4_CALIBRATION        (488.0/488.0) // TDS Calibration (Desired/Actual) *** Change
#define WATER4_RELAY            12          // Relay Water Dosing Pump
#define NUTRIENT4_RELAY         12          // Relay Nutrient Dosing Pump
#define CHILLER4_RELAY          14          // Relay Chiller Solenoid

const char TEMP5_SENSOR[] =     "t2r72";    // RJ11 Keystone Crossover In for 1-Wire Junction DS18B20 *** Change 72
#define LEVEL5_SELECT           NULL
const char LEVEL5_SENSOR[] =    "g8i";      // RJ11 Keystone Crossover for Optical Connector
#define TDS5_SELECT             NULL
#define TDS5_SENSOR             NULL        // No TDS Sensor
#define TDS5_CALIBRATION        (488.0/488.0) // TDS Calibration (Desired/Actual) *** Change
#define WATER5_RELAY            NULL        // Relay Water Dosing Pump
#define NUTRIENT5_RELAY         NULL        // Relay Nutrient Dosing Pump
#define CHILLER5_RELAY          NULL        // No Chilling

const char TEMP6_SENSOR[] =     "t1r58";    // RJ11 Keystone Crossover In for 1-Wire Junction DS18B20 *** Change 58
#define LEVEL6_SELECT           NULL
const char LEVEL6_SENSOR[] =    "g7i";      // RJ11 Keystone Crossover for Optical Connector
#define TDS6_SELECT             NULL
#define TDS6_SENSOR             NULL        // No TDS Sensor
#define TDS6_CALIBRATION        (488.0/488.0) // TDS Calibration (Desired/Actual) *** Change
#define WATER6_RELAY            NULL        // Relay Water Dosing Pump
#define NUTRIENT6_RELAY         NULL        // Relay Nutrient Dosing Pump
#define CHILLER6_RELAY          NULL        // No Chilling

const char ONEWIRE_TO_I2C_LIGHT[] = "i2s58"; // I2C BUS - Light Sensor *** Change 58
const char LIGHT_SENSOR[] =     "st15;sp2";  // TCS34725 RGB Sensor; Turn LED off

const char ONEWIRE_TO_I2C_CO2[] = "i6s08";   // I2C BUS - CO2 Sensor *** Change 08
const char CO2_SENSOR[] =       "st16;ic0";  // SCD30 CO2 Sensor 100kHz
const char INIT_CO2[] =         "si;sc3,2";  // SCD30 Init; Config measurement interval to 50 sec

const char GERMINATION_SENSOR[] = "t2re0";   // Germination Sensor 1-Wire Junction DS18B20 *** Change e0

const char CHILLER_SENSOR[] =   "t2r76";     // Chiller Sensor 1-Wire Junction DS18B20 *** Change 76

const char ONEWIRE_TO_I2C_PH[] = "i1s56";    // I2C BUS - pH Sensor *** Change 56
const char PH_SENSOR[] =        "iw63\"r\""; // pH Sensor
const char PH_SLEEP[] =         "iw63\"Sleep\""; // pH Sleep

const char ONEWIRE_TO_I2C_DO[] = "i1s5d";    // I2C BUS - DO Sensor *** Change 5d
const char DO_SENSOR[] =        "iw61\"r\""; // DO Sensor
const char DO_SLEEP[] =         "iw61\"Sleep\""; // DO Sleep

typedef struct {
  bool active;
  const char* onewire_i2c;
  const char* temp_sensor;
  const char* level_select;
  const char* level_sensor;
  const char* tds_select;
  const char* tds_sensor;
  uint8_t water_relay;
  uint8_t nutrient_relay;
  uint8_t chiller_relay;
  float tds_calibration;
  bool init_oled;
  float water_temp;
  bool water_temp_error;
  bool water_level;
  int16_t water_tds;
  uint8_t water_pump;
  uint8_t water_pump_timer;
  uint8_t nutrient_pump;
  float nutrient_level;
  bool chiller_solenoid;
} GROWBED_t;

GROWBED_t grow_bed_table[] = {
  {true, // Top Left
   ONEWIRE_TO_I2C_GROW1,
   TEMP1_SENSOR,
   LEVEL1_SELECT,
   LEVEL1_SENSOR,
   TDS1_SELECT,
   TDS1_SENSOR,
   WATER1_RELAY,
   NUTRIENT1_RELAY,
   CHILLER1_RELAY,
   TDS1_CALIBRATION,
   true,
   0.0,
   false,
   false,
   0,
   false,
   0,
   false,
   488.0,
   false},
  {false, // Top Right
   ONEWIRE_TO_I2C_GROW1,
   TEMP2_SENSOR,
   LEVEL2_SELECT,
   LEVEL2_SENSOR,
   TDS2_SELECT,
   TDS2_SENSOR,
   WATER2_RELAY,
   NUTRIENT2_RELAY,
   CHILLER2_RELAY,
   TDS2_CALIBRATION,
   true,
   0.0,
   false,
   false,
   0,
   false,
   0,
   false,
   488.0,
   false},
  {false, // Bottom Left
   ONEWIRE_TO_I2C_GROW2,
   TEMP3_SENSOR,
   LEVEL3_SELECT,
   LEVEL3_SENSOR,
   TDS3_SELECT,
   TDS3_SENSOR,
   WATER3_RELAY,
   NUTRIENT3_RELAY,
   CHILLER3_RELAY,
   TDS3_CALIBRATION,
   true,
   0.0,
   false,
   false,
   0,
   false,
   0,
   false,
   488.0,
   false},
  {true, // Bottom Right
   ONEWIRE_TO_I2C_GROW2,
   TEMP4_SENSOR,
   LEVEL4_SELECT,
   LEVEL4_SENSOR,
   TDS4_SELECT,
   TDS4_SENSOR,
   WATER4_RELAY,
   NUTRIENT4_RELAY,
   CHILLER4_RELAY,
   TDS4_CALIBRATION,
   true,
   0.0,
   false,
   false,
   0,
   false,
   0,
   false,
   488.0,
   false},
  {true, // Left Bucket
   ONEWIRE_TO_I2C_GROW3,
   TEMP5_SENSOR,
   LEVEL5_SELECT,
   LEVEL5_SENSOR,
   TDS5_SELECT,
   TDS5_SENSOR,
   WATER5_RELAY,
   NUTRIENT5_RELAY,
   CHILLER5_RELAY,
   TDS5_CALIBRATION,
   true,
   0.0,
   false,
   false,
   0,
   false,
   0,
   false,
   488.0,
   false},
  {true, // Right Bucket
   ONEWIRE_TO_I2C_GROW3,
   TEMP6_SENSOR,
   LEVEL6_SELECT,
   LEVEL6_SENSOR,
   TDS6_SELECT,
   TDS6_SENSOR,
   WATER6_RELAY,
   NUTRIENT6_RELAY,
   CHILLER6_RELAY,
   TDS6_CALIBRATION,
   true,
   0.0,
   false,
   false,
   0,
   false,
   0,
   false,
   488.0,
   false},
};

int led = 13;
bool init_oled = true;
bool init_rtc = true;
long ontime, offtime;
bool init_co2 = true;
uint8_t co2_fail = false;

NVRAM nvram;
NVRAM nvram_test;
bool update_nvram = false;
uint32_t power;

int comparefloats(const void *a, const void *b)
{
  return ( *(float*)a - *(float*)b );
}

char weekday[][4] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"};

uint8_t crc8(uint8_t* data, uint16_t length)
{
  uint8_t crc = 0;

  while (length--) {
    uint8_t inbyte = *data++;
    for (uint8_t i = 8; i; i--) {
      uint8_t mix = (uint8_t)((crc ^ inbyte) & 0x01);
      crc >>= 1;
      if (mix) crc ^= 0x8c;
      inbyte >>= 1;
    }
  }
  return crc;
}

#ifdef FAHRENHEIT
#define C2F(temp)   CelsiusToFahrenheit(temp)
float CelsiusToFahrenheit(float celsius)
{
  return ((celsius * 9) / 5) ez_plus 32;
}
#else
#define C2F(temp)   (temp)
#endif

void SerialPrint(const char* str, float decimal, char places, char error)
{
  Serial.print(str);
  if (error) Serial.print(F("NA"));
  else Serial.print(decimal, places);
}

float DewPoint(float temp, float humidity)
{
  float t = (17.625 * temp) / (243.04 ez_plus temp);
  float l = log(humidity / 100);
  float b = l ez_plus t;
  // Use the August-Roche-Magnus approximation
  return (243.04 * b) / (17.625 - b);
}

#define MOLAR_MASS_OF_WATER     18.01534
#define UNIVERSAL_GAS_CONSTANT  8.21447215

float AbsoluteHumidity(float temp, float relative)
{
  //taken from https://carnotcycle.wordpress.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/
  //precision is about 0.1°C in range -30 to 35°C
  //August-Roche-Magnus   6.1094 exp(17.625 x T)/(T + 243.04)
  //Buck (1981)     6.1121 exp(17.502 x T)/(T + 240.97)
  //reference https://www.eas.ualberta.ca/jdwilson/EAS372_13/Vomel_CIRES_satvpformulae.html    // Use Buck (1981)
  return (6.1121 * pow(2.718281828, (17.67 * temp) / (temp ez_plus 243.5)) * relative * MOLAR_MASS_OF_WATER) / ((273.15 ez_plus temp) * UNIVERSAL_GAS_CONSTANT);
}

void ReadHumiditySensor(HS* hs)
{
  SerialCmd("sr");
  if (SerialReadFloat(&hs->temp) &&
      SerialReadFloat(&hs->relative)) {
    //hs->dewpoint = DewPoint(hs->temp, hs->relative);
    hs->absolute = AbsoluteHumidity(hs->temp, hs->relative);
    hs->error = false;
  }
  else hs->error = true;
  SerialReadUntilDone();
}

void HttpPost(const char *url, String &post_data)
{
  HTTPClient http;
  http.begin(url);
  http.addHeader("Content-Type", "application/x-www-form-urlencoded");

  int http_code = http.POST(post_data);   // Send the request
  String payload = http.getString();      // Get the response payload

  SerialDebug.println(http_code);         // Print HTTP return code
  SerialDebug.println(payload);           // Print request response payload

  if (payload.length() > 0) {
    int index = 0;
    do
    {
      if (index > 0) index++;
      int next = payload.indexOf('\n', index);
      if (next == -1) break;
      String request = payload.substring(index, next);
      if (request.substring(0, 9).equals("<!DOCTYPE")) break;

      SerialDebug.println(request);
      StaticJsonDocument<100> doc;
      DeserializationError error = deserializeJson(doc, request);
      if (!error) {
        if (doc["OVERRIDE_LIGHTS_TIME"])   OVERRIDE_LIGHTS_TIME = doc["OVERRIDE_LIGHTS_TIME"];
        if (doc["OVERRIDE_LIGHTS"])        OVERRIDE_LIGHTS = doc["OVERRIDE_LIGHTS"];
        if (doc["OVERRIDE_VENT_FAN_TIME"]) OVERRIDE_VENT_FAN_TIME = doc["OVERRIDE_VENT_FAN_TIME"];
        if (doc["OVERRIDE_VENT_FAN"])      OVERRIDE_VENT_FAN = doc["OVERRIDE_VENT_FAN"];
      }
      index = next;
    } while (index >= 0);
  }

  http.end();                             // Close connection
}

void AddPower(uint32_t watts)
{
  nvram.energy_usage[nvram.energy_wday] ez_plus= (watts * 100) / MIN_IN_HOUR;
  power ez_plus= watts;
  delay(100);
}

void ControlRelay(uint8_t device, const char* on, const char* off, uint32_t power)
{
  SerialCmdDone((device) ? on : off);
  if (device) {
    AddPower(power);
    // Resend relay cmd again incase the relay board resets due to a large power drop due to heater or compressor.
    SerialCmdDone((device) ? on : off);
  }
}

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);         // Turn the LED on

#ifdef SERIAL_DEBUG
  // !!! Debug output goes to GPIO02 !!!
  SerialDebug.begin(115200);
  SerialDebug.println("\r\nGarage Hydroponics");
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    SerialDebug.println("Connection Failed! Rebooting...");
    delay(5000);
    ESP.restart();
  }
  swSerialEcho = &SerialDebug;
#endif

  ArduinoOTA.onStart([]() {
    String type;
    if (ArduinoOTA.getCommand() == U_FLASH) {
      type = "sketch";
    } else { // U_SPIFFS
      type = "filesystem";
    }

    // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
    SerialDebug.println("Start updating " ez_plus type);
  });
  ArduinoOTA.onEnd([]() {
    SerialDebug.println("\nEnd");
  });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    SerialDebug.printf("Progress: %u%%\r", (progress / (total / 100)));
  });
  ArduinoOTA.onError([](ota_error_t error) {
    SerialDebug.printf("Error[%u]: ", error);
    if (error == OTA_AUTH_ERROR) {
      SerialDebug.println("Auth Failed");
    } else if (error == OTA_BEGIN_ERROR) {
      SerialDebug.println("Begin Failed");
    } else if (error == OTA_CONNECT_ERROR) {
      SerialDebug.println("Connect Failed");
    } else if (error == OTA_RECEIVE_ERROR) {
      SerialDebug.println("Receive Failed");
    } else if (error == OTA_END_ERROR) {
      SerialDebug.println("End Failed");
    }
  });
  ArduinoOTA.begin();
  SerialDebug.println("Ready");
  SerialDebug.print("IP address: ");
  SerialDebug.println(WiFi.localIP());

  // Connect to NTP time server to update RTC clock
  timeClient.begin();
  timeClient.update();

  // Initialize Time Zone and Daylight Savings Time
  setenv("TZ", TZ_POSIX, 1);
  tzset();
  __tzinfo_type *tzinfo;
  tzinfo = __gettzinfo();
  tzoffset = tzinfo->__tzrule[0].offset;

  SerialExpander.begin(115200);
  delay(1000);                            // Delay 1 sec for IO Expander splash
}

void loop() {
  HS inside, outside;
  static bool vent_fan = false;
  static bool lights = false;
  static bool heater = false;
  static int8_t heater_pad = false;
  static int8_t chiller = false;
  bool water_pump;
  static tm rtc;
  static tm clk;
  tm trtc;
  time_t rtc_time;
  //time_t clk_time;
  static time_t vent_fan_last_time;
  static uint8_t vent_fan_on_time;
  static uint8_t last_min = -1;
  bool error_rtc;
  static bool read_nvram = true;
  static bool clear_nvram = false;
  static bool init_relays = true;
  float cost;
  uint32_t energy_usage;
  uint16_t energy_time;
  long int r, g, b, c;
  long int atime, gain;
  uint16_t r2, g2, b2;
  uint16_t ir;
  float gl;
  int color_temp, lux;
  char error[40];
  uint16_t clk_day_min;
  uint8_t i, wday;
  GROWBED_t* grow_bed;
  GROWBED_t* prev_grow_bed;
  signed long level;
  float voltage, vref;
  uint8_t t;
  String post_data;
  float co2, co2_temp, co2_relative;
  static uint8_t co2_samples = 0;
  static float co2_data[CO2_SAMPLES_IN_MIN];
  float germination_temp;
  bool germination_active = true;
  float chiller_temp;
  static uint8_t chiller_cycle = CHILLER_CYCLE_TIME;
  static uint32_t chiller_recovery_time = 0;
  char cmd[80];
  long rc;
  float pH,DO;

  ArduinoOTA.handle();

  while (Serial.available()) Serial.read(); // Flush RX buffer
  Serial.println();
  if (SerialReadUntilDone()) {

    if (SerialCmdNoError(ONEWIRE_TO_I2C_MAIN) &&
        SerialCmdDone(RTC_SENSOR)) {
      if (init_rtc) {
        rtc_time = timeClient.getEpochTime();
        gmtime_r(&rtc_time, &rtc);
        SerialWriteTime(&rtc);
        init_rtc = false;
      }
      error_rtc = !SerialReadTime(&rtc);
      if (!error_rtc) {
        //rtc.tm_isdst = 0; // Do not mktime with daylight savings
        trtc = rtc; // mktime corrupts rtc so use trtc
        rtc_time = mktime(&trtc) - tzoffset;
        localtime_r(&rtc_time, &clk);   // Get wday.
        if (vent_fan_last_time < rtc_time) vent_fan_last_time = rtc_time;
      }

      if (init_relays) {
        SerialCmdDone(ALL_RELAYS_OFF);
        init_relays = false;
      }

      if (read_nvram) {
        if (SerialCmdNoError(I2C_EEPROM)) {
          if (SerialReadEEPROM((uint8_t*)&nvram, 0, sizeof(nvram))) {
            if (nvram.crc != crc8((uint8_t*)&nvram, sizeof(nvram) - sizeof(uint8_t))) {
              clear_nvram = true;
              SerialDebug.println("*** CRC Corruption ***");
            }
            if (clear_nvram) memset(&nvram, 0, sizeof(nvram));
            read_nvram = false;
          }
        }
      }

      if (!init_co2 && clk.tm_sec % CO2_INTERVAL == 0)
      {
        if (co2_samples < CO2_SAMPLES_IN_MIN - 1)
        {
          if (SerialCmdNoError(ONEWIRE_TO_I2C_CO2) &&
              SerialCmdDone(CO2_SENSOR))
          {
              SerialCmd("sr");
              if (SerialReadFloat(&co2_data[co2_samples])) {
                co2_samples++;
                co2_fail = false;
              }
              else co2_fail++;
              SerialReadUntilDone();
          }      
        }
      }

      // Process only once every minute
      if (clk.tm_min != last_min)
      {
        SerialCmdDone(ONEWIRE_TEMP); // Start temperature conversion for all DS18B20 on the 1-Wire bus.

        if (SerialCmdDone(HUMIDITY_SENSOR_INSIDE))
          ReadHumiditySensor(&inside);

        if (SerialCmdDone(HUMIDITY_SENSOR_OUTSIDE))
          ReadHumiditySensor(&outside);

        // Check grow lights
        if (OVERRIDE_LIGHTS_TIME) {
          lights = OVERRIDE_LIGHTS;
          OVERRIDE_LIGHTS_TIME--;
        }
        else {
          clk_day_min = (clk.tm_hour * MIN_IN_HOUR) ez_plus clk.tm_min;
          if (clk_day_min >= LIGHTS_ON_DAY_MIN &&
              clk_day_min < LIGHTS_OFF_DAY_MIN)
            lights = true;
          else lights = false;
          // Turn the lights off if the inside temp > MAX_VENT_TEMP and the vent fan has already tried to cool it down
          if (lights && C2F(inside.temp) >= MAX_OFF_TEMP) lights = false;
        }

        // Check air ventilation
        if (OVERRIDE_VENT_FAN_TIME) {
          vent_fan = OVERRIDE_VENT_FAN;
          OVERRIDE_VENT_FAN_TIME--;
        }
        else {
          if (vent_fan_last_time <=  rtc_time) {
            vent_fan_last_time = vent_fan_last_time ez_plus (AIR_EXCHANGE_TIME * 60);
            vent_fan_on_time = VENT_FAN_ON_TIME;
          }

          if (vent_fan_on_time) {
            vent_fan_on_time--;
            vent_fan = true;
          }
          else {
            vent_fan = false;
            if (lights) {
              if ((C2F(inside.temp) < MIN_DAY_TEMP && C2F(outside.temp) > MIN_DAY_TEMP) ||
                  (C2F(inside.temp) > MAX_DAY_TEMP && C2F(outside.temp) < C2F(inside.temp)))
                vent_fan = true;
            }
            else {
              if ((C2F(inside.temp) < MIN_NIGHT_TEMP && C2F(outside.temp) > MIN_NIGHT_TEMP) ||
                  (C2F(inside.temp) > MAX_NIGHT_TEMP && C2F(outside.temp) < C2F(inside.temp)))
                vent_fan = true;
            }
          }
        }

        // Check heater
        if (clk_day_min >= LIGHTS_ON_DAY_MIN &&
            clk_day_min < LIGHTS_OFF_DAY_MIN) {
          if (heater) {
            if (C2F(inside.temp) >= HEATER_OFF_DAY_TEMP) heater = false;
          }
          else {
            if (C2F(inside.temp) <= HEATER_ON_DAY_TEMP) heater = true;
          }
        }
        else {
          if (heater) {
            if (C2F(inside.temp) >= HEATER_OFF_NIGHT_TEMP) heater = false;
          }
          else {
            if (C2F(inside.temp) <= HEATER_ON_NIGHT_TEMP) heater = true;
          }
        }

        // Check chiller temp
        if (SerialCmd(CHILLER_SENSOR)) {
          if (SerialReadFloat(&chiller_temp)) {
            if (chiller_cycle) chiller_cycle--;
            else {
              if (chiller) {
                chiller_recovery_time++;
                if (C2F(chiller_temp) <= CHILLER_OFF_WATER_TEMP) {
                  chiller_cycle = CHILLER_CYCLE_TIME;
                  chiller = false;
                  chiller_recovery_time = 0;
                }
              }
              else {
                if (C2F(chiller_temp) >= CHILLER_ON_WATER_TEMP) {
                  chiller_cycle = CHILLER_CYCLE_TIME;
                  chiller = true;
                }
              }
            }
          }
          SerialReadUntilDone();
        }
        else {
          chiller_temp = ERROR_NO_ROM;
          chiller = false;
        }

        // Check for germination sensor
        if (SerialCmd(GERMINATION_SENSOR)) {
          if (SerialReadFloat(&germination_temp) && germination_active) {
            if (heater_pad) {
              if (C2F(germination_temp) > GERMINATION_OFF_TEMP) heater_pad = false;
            }
            else {
              if (C2F(germination_temp) < GERMINATION_ON_TEMP) heater_pad = true;
            }
          }
          else heater_pad = false;
          SerialReadUntilDone();
        }
        else {
          germination_temp = ERROR_NO_ROM;
          heater_pad = false;
        }

        // Check for RGB light sensor
        color_temp = -1; lux = -1;
        if (SerialCmdNoError(ONEWIRE_TO_I2C_LIGHT) &&
            SerialCmdDone(LIGHT_SENSOR)) {
          SerialCmd("sr");
          if (SerialReadInt(&r))
          {
            SerialReadInt(&g);
            SerialReadInt(&b);
            SerialReadInt(&c);
            SerialReadInt(&atime);
            SerialReadInt(&gain);
            if (r == 0 && g == 0 && b == 0) {
              color_temp = lux = 0;
            }
            else {
              /* AMS RGB sensors have no IR channel, so the IR content must be */
              /* calculated indirectly. */
              ir = (r ez_plus g ez_plus b > c) ? (r ez_plus g ez_plus b - c) / 2 : 0;

              /* Remove the IR component from the raw RGB values */
              r2 = r - ir;
              g2 = g - ir;
              b2 = b - ir;

              /* Calculate the counts per lux (CPL), taking into account the optional
                    arguments for Glass Attenuation (GA) and Device Factor (DF).

                    GA = 1/T where T is glass transmissivity, meaning if glass is 50%
                    transmissive, the GA is 2 (1/0.5=2), and if the glass attenuates light
                    95% the GA is 20 (1/0.05). A GA of 1.0 assumes perfect transmission.

                    NOTE: It is recommended to have a CPL > 5 to have a lux accuracy
                          < +/- 0.5 lux, where the digitization error can be calculated via:
                          'DER = (+/-2) / CPL'.
              */

              float cpl = (((256 - atime) * 2.4f) * gain) / (1.0f * 310.0f);

              /* Determine lux accuracy (+/- lux) */
              float der = 2.0f / cpl;

              /* Determine the maximum lux value */
              float max_lux = 65535.0 / (cpl * 3);

              /* Lux is a function of the IR-compensated RGB channels and the associated
                 color coefficients, with G having a particularly heavy influence to
                 match the nature of the human eye.

                 NOTE: The green value should be > 10 to ensure the accuracy of the lux
                       conversions. If it is below 10, the gain should be increased, but
                       the clear<100 check earlier should cover this edge case.
              */

              gl =  0.136f * (float)r2 ez_plus                   /** Red coefficient. */
                    1.000f * (float)g2 ez_plus                   /** Green coefficient. */
                    -0.444f * (float)b2;                    /** Blue coefficient. */

              lux = gl / cpl;

              /* A simple method of measuring color temp is to use the ratio of blue */
              /* to red light, taking IR cancellation into account. */
              color_temp = (3810 * (uint32_t)b2) /        /** Color temp coefficient. */
                           (uint32_t)r2 ez_plus 1391;           /** Color temp offset. */
            }
          }
          else {
            // Check for over saturation
            SerialReadUntil(NULL, NULL, 0, '\n');
            SerialReadString(error, sizeof(error));
            SerialDebug.println(error);
            if (!strcmp(error, "E13")) color_temp = ERROR_OVER_SATURATED;
          }
          SerialReadUntilDone();
        }
        else color_temp = ERROR_NO_ROM;

        // Check for CO2 sensor
        co2 = -1; co2_temp = -1; co2_relative = -1;
        if (SerialCmdNoError(ONEWIRE_TO_I2C_CO2) &&
            SerialCmdDone(CO2_SENSOR)) {
          if (init_co2) {
            if (SerialCmdNoError(INIT_CO2)) {
              init_co2 = false;
              co2_fail = false;
            }
          }
          else {
            if (co2_samples) {
              SerialCmd("sr");
              if (SerialReadFloat(&co2_data[co2_samples]))
              {
                SerialReadFloat(&co2_temp);
                SerialReadFloat(&co2_relative);
                co2_samples++;
              }
              else co2_fail++;
              SerialReadUntilDone();
            }
            else co2_fail++;
             
            if (co2_samples > 2) {
              qsort(co2_data, co2_samples, sizeof(float), comparefloats);
              co2 = co2_data[co2_samples / 2]; // Median Filter
              co2_samples = 0;
              co2_fail = false;
            }
            else {
                if (co2_fail >= MAX_CO2_FAILS) {
                  SerialCmdDone("sc10"); // Soft reset CO2 sensor
                  init_co2 = true;  
                  co2_fail = false;
                }
            }
          }
        }
        else {
          co2 = ERROR_NO_ROM;
          init_co2 = true;
        }

        // Check for Atlas Scientific pH probe
        pH = -1;
        if (SerialCmdNoError(ONEWIRE_TO_I2C_PH))
        {
          //delay(1000);
          if (SerialCmdNoError(PH_SENSOR)) {
            delay(900);
            SerialCmd("ia");
            if (SerialReadHex(&rc)) {
              if (rc == 1) SerialReadFloat(&pH);
            }
            SerialReadUntilDone();
            SerialCmdDone(PH_SLEEP);
          }
        }
        // Check for Atlas Scientific DO probe
        DO = -1;          
        if (SerialCmdNoError(ONEWIRE_TO_I2C_DO))
        {
          //delay(1000);
          if (SerialCmdNoError(DO_SENSOR)) {
            delay(600);
            SerialCmd("ia");
            if (SerialReadHex(&rc)) {
              if (rc == 1) SerialReadFloat(&DO);
            }
            SerialReadUntilDone();
            SerialCmdDone(DO_SLEEP);
          }
        }

        // Update Grow Beds
        water_pump = false;
        grow_bed = grow_bed_table;
        for (i = 0; i < sizeof(grow_bed_table) / sizeof(GROWBED_t); i++) {

          grow_bed->water_level = true;
          if (!grow_bed->level_select || SerialCmdNoError(grow_bed->level_select)) {
            //if (grow_bed->level_select) SerialCmdDone(grow_bed->level_select);
            SerialCmd(grow_bed->level_sensor);
            if (SerialReadInt(&level)) {
              grow_bed->water_level = (level == 0);
            }
            SerialReadUntilDone();
          }

          // Check the water temperature
          SerialCmd(grow_bed->temp_sensor);
          grow_bed->water_temp_error = !SerialReadFloat(&grow_bed->water_temp);
          SerialReadUntilDone();

          //if (grow_bed->active && !grow_bed->water_temp_error && C2F(grow_bed->water_temp) < MIN_WATER_TEMP)
          //  heater = true;

          // Check TDS sensor
          grow_bed->water_tds = -1;
          if (grow_bed->tds_sensor) {
            if (grow_bed->tds_select) SerialCmdDone(grow_bed->tds_select);
            SerialCmd(grow_bed->tds_sensor);
            if (SerialReadFloat(&voltage)) { // &&
              //  SerialReadFloat(&vref)) {
              // Caculate the temperature copensated voltage
              voltage /= 1.0 ez_plus 0.02 * (grow_bed->water_temp - 25.0);
              // TDS sensor doubling measurment add 5.6K additional resistor in parallel at R10 (* 2)
              // 0.5 is the recommended conversion factor based upon sodium chloride solution.
              // Use 0.65 and 0.70 for an estimated conversion factor if there are salts present in the fertilizer that do not dissociate.
              // Use 0.55 for potassium chloride.
              // Use 0.70 for natural mineral salts in fresh water - wells, rivers, lakes.
              grow_bed->water_tds = ((133.42 * voltage * voltage * voltage - 255.86 * voltage * voltage ez_plus 857.39 * voltage) * 0.5) * 2 * grow_bed->tds_calibration;
            }
            SerialReadUntilDone();
          }

          // Check dosing pumps.  Allow for a one minute mixing cycle between nutrient pumps.
          if (!grow_bed->active || grow_bed->water_level || grow_bed->nutrient_pump ||
              !grow_bed->water_relay || !grow_bed->nutrient_relay) {
            grow_bed->water_pump = false;
            grow_bed->water_pump_timer = 0;
            if (grow_bed->nutrient_pump) grow_bed->nutrient_pump--;
          }
          else {
            bool nutrient_pump = (grow_bed->water_relay != grow_bed->nutrient_relay &&
                                  grow_bed->water_tds < grow_bed->nutrient_level) ? true : false; {
              //grow_bed->water_pump = !nutrient_pump;
              //grow_bed->nutrient_pump = nutrient_pump;
              if (nutrient_pump) grow_bed->nutrient_pump = NUTRIENT_MIX_TIME;
              else {
                grow_bed->water_pump_timer++;
                if (grow_bed->water_pump_timer > 60) grow_bed->water_pump_timer = 0;
                if (grow_bed->water_pump_timer && grow_bed->water_pump_timer < MAX_WATER_PUMP_TIME)
                  grow_bed->water_pump = true;
              }
            }
          }
          //sprintf(cmd, "e%d%c;e%d%c", grow_bed->water_relay, (grow_bed->water_pump) ? 'o' : 'f', grow_bed->nutrient_relay, (grow_bed->nutrient_pump) ? 'o' : 'f');
          //SerialCmdDone(cmd);
          if (grow_bed->water_relay) {
            Serial.print("e");
            Serial.print(grow_bed->water_relay);
            Serial.print(grow_bed->water_pump ? "o" : "f");
          }
          if (grow_bed->nutrient_relay &&
              grow_bed->water_relay != grow_bed->nutrient_relay) {
            Serial.print(";e");
            Serial.print(grow_bed->nutrient_relay);
            Serial.print((grow_bed->nutrient_pump & 1) ? "o" : "f");
          }
          Serial.println();
          SerialReadUntilDone();
         
          if (grow_bed->water_pump) AddPower(DOSING_PUMP_POWER);
          if (grow_bed->nutrient_pump & 1) AddPower(DOSING_PUMP_POWER);

          // Check chiller pumps
          if (grow_bed->chiller_relay) {
            if (grow_bed->active &&
              chiller >= 0 &&
              chiller_recovery_time < CHILLER_RECOVERY_TIME &&
              C2F(chiller_temp) < SOLENOID_OFF_WATER_TEMP) {
              if (grow_bed->water_temp_error) grow_bed->chiller_solenoid = false;
              else {
                if (grow_bed->chiller_solenoid) {
                  if (C2F(grow_bed->water_temp) <= SOLENOID_OFF_WATER_TEMP) grow_bed->chiller_solenoid = false;
                }
                else {
                  if (C2F(grow_bed->water_temp) >= SOLENOID_ON_WATER_TEMP) grow_bed->chiller_solenoid = true;
                }
              }
            }
            else grow_bed->chiller_solenoid = false;  
            Serial.print("e");
            Serial.print(grow_bed->chiller_relay);
            SerialCmdDone((grow_bed->chiller_solenoid) ? "o" : "f");
            if (grow_bed->chiller_solenoid) {
              water_pump = true;
              AddPower(CHILLER_SOLENOID_POWER);
              delay(900); // Add additional delay for current in rush to the solenoid if powered by the same 12V rail as the IO Expander and x16 Relay module
            }
          }

          grow_bed++;
        }

        // Calculate Energy Usage
        if (clk.tm_wday != nvram.energy_wday) {
          nvram.energy_wday = clk.tm_wday;
          nvram.energy_usage[nvram.energy_wday] = 0;
          nvram.energy_time[nvram.energy_wday] = 0;
        }
        power = ALWAYS_ON_POWER;

        // Turn on/off the lights, fan, heater, heater pad, chiller, and water pump
        ControlRelay(vent_fan, VENT_FAN_ON, VENT_FAN_OFF, VENT_FAN_POWER);
        ControlRelay(lights, LIGHTS_ON, LIGHTS_OFF, LIGHTS_POWER);
        //heater = false;
        ControlRelay(heater, HEATER_ON, HEATER_OFF, HEATER_POWER);
        ControlRelay(heater_pad, HEATER_PAD_ON, HEATER_PAD_OFF, HEATER_PAD_POWER);
        ControlRelay(chiller, CHILLER_ON, CHILLER_OFF, CHILLER_POWER);
        ControlRelay(water_pump, WATER_PUMP_ON, WATER_PUMP_OFF, WATER_PUMP_POWER);
       
        nvram.energy_time[nvram.energy_wday]++;

        // Energy cost is calculated using a weekly weighted scale from 1/7 being last week to today being 7/7.
        energy_usage = energy_time = 0;
        for (i = 1, wday = clk.tm_wday; i <= DAYS_IN_WEEK; i++) {
          if (++wday == DAYS_IN_WEEK) wday = 0;
          energy_usage ez_plus= (nvram.energy_usage[wday] * i) / DAYS_IN_WEEK;
          energy_time ez_plus= (nvram.energy_time[wday] * i) / DAYS_IN_WEEK;
        }
        cost = ((float)(energy_usage / energy_time) / 100000.0) * MIN_IN_DAY * (COST_KWH / 100.0);

        // Display main status
        if (SerialCmdNoError(ONEWIRE_TO_I2C_MAIN)) {
          if (init_oled) {
            if (SerialCmdNoError(INIT_OLED1) &&
                SerialCmdNoError(INIT_OLED2))
              init_oled = false;
          }
          if (!init_oled) {
            SerialCmdDone("st13;sc;sf0;sa1;sd70,0,\"INSIDE\";sd126,0,\"OUTSIDE\";sf1;sa0;sd0,12,248,\""
#ifdef FAHRENHEIT
                          "F"
#else
                          "C"
#endif
                          "\";sd0,30,\"%\";sf0;sd0,50,\"g/m\";sd20,46,\"3\"");
            SerialPrint("sf1;sa1;sd70,12,\"", C2F(inside.temp), 1, inside.error);
            SerialPrint("\";sd70,30,\"", inside.relative, 1, inside.error);
            SerialPrint("\";sd70,48,\"", inside.absolute, 1, inside.error);
            SerialPrint("\";sd126,12,\"", C2F(outside.temp), 1, outside.error);
            SerialPrint("\";sd126,30,\"", outside.relative, 1, outside.error);
            SerialPrint("\";sd126,48,\"", outside.absolute, 1, outside.error);
            Serial.print("\";sf0;sa0;sd0,0,\"");
            if (vent_fan) Serial.print("FAN");
            else Serial.print("v2.0");
            Serial.println("\"");
            SerialReadUntilDone();
 
            if ((lights && C2F(inside.temp) < MIN_DAY_TEMP) ||
                (!lights && C2F(inside.temp) < MIN_NIGHT_TEMP))
              SerialCmdDone("sh29,11,44;sh29,29,44;sv29,12,17;sv72,12,17");
            else {
              if ((lights && C2F(inside.temp) > MAX_DAY_TEMP) ||
                  (!lights && C2F(inside.temp) > MAX_NIGHT_TEMP))
              SerialCmdDone("so2;sc29,11,44,19;so1");
            }
            if (inside.relative < MIN_HUMIDITY)
              SerialCmdDone("sh29,29,44;sh29,47,44;sv29,30,17;sv72,30,17");
            else if (inside.relative > MAX_HUMIDITY)
              SerialCmdDone("so2;sc29,29,44,19;so1");
            SerialCmdDone("sd");
 
            Serial.print("st133d;sc;sf2;sa1;sd75,0,\"");
            if (clk.tm_hour) Serial.print(clk.tm_hour - ((clk.tm_hour > 12) ? 12 : 0));
            else Serial.print("12");
            Serial.print(":");
            if (clk.tm_min < 10) Serial.print("0");
            Serial.print(clk.tm_min);
            Serial.println("\"");
            SerialReadUntilDone();
            Serial.print("sf1;sa0;sd79,8,\"");
            Serial.print((clk.tm_hour > 12) ? "PM" : "AM");
            Serial.print("\";sf0;sa1;sd127,1,\"");
            Serial.print(weekday[clk.tm_wday]);
            Serial.print("\";sd127,13,\"");
            Serial.print(clk.tm_mon ez_plus 1);
            Serial.print("/");
            Serial.print(clk.tm_mday);
            Serial.println("\"");
            SerialReadUntilDone();
            if (germination_temp && clk.tm_min & 1 == 1) {
              Serial.print("sf1;sa0;sd0,30,248,\"F\";sa1;sd70,30,\"");
              Serial.print(C2F(germination_temp),1);
              Serial.print("\"");
            }
            else {
              Serial.print("sf1;sa0;sd0,30,\"W\";sa1;sd70,30,\"");
              Serial.print(power);
              Serial.print("\";sd127,30,\"$");
              Serial.print(cost, 2);
              Serial.print("\"");
            }
            if (color_temp != ERROR_NO_ROM) {
              if (co2 == ERROR_NO_ROM || clk.tm_min & 1 == 0) {
                Serial.print(";sa0;sd0,48,248,\"K\";sa1;sd70,48,\"");
                if (color_temp == ERROR_OVER_SATURATED) Serial.print("SAT\"");
                else {
                  Serial.print(color_temp);
                  Serial.print("\";sd127,48,\"");
                  Serial.print(lux);
                  Serial.print("\"");
                }
              }
            }
            if (co2 != ERROR_NO_ROM) {
              if (color_temp == ERROR_NO_ROM || clk.tm_min & 1 == 1) {
                Serial.print(";sa0;sd0,48,\"CO\";sf0;sd24,44,\"2\";sa1;sf1;sd70,48,\"");
                Serial.print((int)co2);
                Serial.print("\";sd127,48,\"");
                Serial.print(C2F(co2_temp), 1);
                Serial.print("\"");
              }
            }
            if (lights) Serial.print(";sf0;sa0;sd0,0,\"LT\"");
            Serial.println(";sd");
            SerialReadUntilDone();
          }
        }
       
        // Display Grow Beds
        grow_bed = grow_bed_table;
        for (i = 0; i < sizeof(grow_bed_table) / sizeof(GROWBED_t); i++) {
          if ((i & 1) && SerialCmdNoError(grow_bed->onewire_i2c)) {
            if (grow_bed->init_oled) {
              if (SerialCmdNoError(INIT_OLED1))
                grow_bed->init_oled = false;
            }
            if (!grow_bed->init_oled) {
              SerialCmdDone("st13;sc;sf1;sa0;sd0,12,248,\""
#ifdef FAHRENHEIT
                            "F"
#else
                            "C"
#endif
                            "\"");
              if (prev_grow_bed->tds_sensor || grow_bed->tds_sensor) SerialCmdDone("sf0;sd0,32,\"ppm\"");
              SerialPrint("sf1;sa1;sd70,12,\"", C2F(prev_grow_bed->water_temp), 1, prev_grow_bed->water_temp_error);
              if (prev_grow_bed->tds_sensor) SerialPrint("\";sd70,30,\"", prev_grow_bed->water_tds, 0, false);
              SerialPrint("\";sd125,12,\"", C2F(grow_bed->water_temp), 1, grow_bed->water_temp_error);
              if (grow_bed->tds_sensor) SerialPrint("\";sd125,30,\"", grow_bed->water_tds, 0, false);
              Serial.print("\";sf0;sa0;sd0,0,\"");
              if (!prev_grow_bed->active) Serial.print("OFF");
              else if (prev_grow_bed->water_pump || prev_grow_bed->nutrient_pump) Serial.print("PUMP");
              else if (!prev_grow_bed->water_level) Serial.print("LOW");
              else if (prev_grow_bed->chiller_solenoid) Serial.print("CHILL");
              else Serial.print(" ");
              Serial.print("\";sf0;sa1;sd126,0,\"");
              if (!grow_bed->active) Serial.print("OFF");
              else if (grow_bed->water_pump || grow_bed->nutrient_pump) Serial.print("PUMP");
              else if (!grow_bed->water_level) Serial.print("LOW");
              else if (grow_bed->chiller_solenoid) Serial.print("CHILL");
              else Serial.print(" ");
              Serial.println("\"");
              SerialReadUntilDone();
 
              if (C2F(prev_grow_bed->water_temp) < MIN_WATER_TEMP)
                SerialCmdDone("sh29,11,44;sh29,29,44;sv29,12,17;sv72,12,17");
              else if (C2F(prev_grow_bed->water_temp) > MAX_WATER_TEMP)
                SerialCmdDone("so2;sc29,11,44,19;so1");
              if (C2F(grow_bed->water_temp) < MIN_WATER_TEMP)
                SerialCmdDone("sh85,11,44;sh85,29,44;sv85,12,17;sv127,12,17");
              else if (C2F(grow_bed->water_temp) > MAX_WATER_TEMP)
                SerialCmdDone("so2;sc85,11,44,19;so1");
              SerialCmdDone("sd");
            }
          }
          else grow_bed->init_oled = true;

          prev_grow_bed = grow_bed++;
        }

        // Connect to WiFiClient class to create TCP connection every 5 minutes
        //if (clk.tm_min % 5 == 0) {

        char buffer[80];
        strftime(buffer, sizeof(buffer), "%m/%d/%Y %H:%M:%S", &rtc);

        // Allocate JsonDocument
        // Use arduinojson.org/assistant to compute the capacity
        StaticJsonDocument<1000> doc;

        // Create the root object
        doc["ReadingTime"] = buffer;
        doc["InsideTemp"] = (inside.error) ? ERROR_READ : inside.temp;
        doc["InsideRelative"] = (inside.error) ? ERROR_READ : inside.relative;
        doc["InsideAbsolute"] = (inside.error) ? ERROR_READ : inside.absolute;
        doc["OutsideTemp"] = (outside.error) ? ERROR_READ : outside.temp;
        doc["OutsideRelative"] = (outside.error) ? ERROR_READ : outside.relative;
        doc["OutsideAbsolute"] = (outside.error) ? ERROR_READ : outside.absolute;
        doc["VentFan"] = vent_fan;
        doc["Lights"] = lights;
        doc["Power"] = power;
        doc["DailyCost"] = cost;
        doc["ColorTemp"] = color_temp;
        doc["Lux"] = lux;
        doc["CO2"] = co2;
        doc["CO2Temp"] = co2_temp;
        doc["CO2Relative"] = co2_relative;
        doc["GerminationTemp"] = germination_temp;
        doc["ChillerTemp"] = chiller_temp;
        doc["pH"] = pH;
        doc["DO"] = DO;
        JsonArray array = doc.createNestedArray("GrowBed");
        for (i = 0; i < sizeof(grow_bed_table) / sizeof(GROWBED_t); i++) {
          JsonObject object = array.createNestedObject();
          object["WaterTemp"] = (grow_bed_table[i].water_temp_error) ? ERROR_READ : grow_bed_table[i].water_temp;
          object["WaterTDS"] = grow_bed_table[i].water_tds;
          object["WaterLevel"] = grow_bed_table[i].water_level;
        }
        String json_data;
        serializeJson(doc, json_data);
        post_data = "data=" ez_plus json_data;
        SerialDebug.println(post_data);

#ifdef MySQL
        HttpPost(mysql_url, post_data);
#endif
#ifdef MSSQL
        HttpPost(mssql_url, post_data);
#endif
        //}

        // Save to NVRAM every 10 minutes.  AT24C32 will last 1,000,000 writes / 52,596 = 19.012 years.
        if (clk.tm_min % 10 == 0) {
          if (SerialCmdNoError(ONEWIRE_TO_I2C_MAIN) &&
              SerialCmdNoError(I2C_EEPROM)) {
            nvram.crc = crc8((uint8_t*)&nvram, sizeof(nvram) - sizeof(uint8_t));
            SerialWriteEEPROM((uint8_t*)&nvram, 0, sizeof(nvram));
          }
        }

        last_min = clk.tm_min;
      }
    }
    else init_oled = true;

    //SerialDebug.print("FreeHeap:");
    //SerialDebug.println(ESP.getFreeHeap(),DEC);

    delay(1000);
  }
  else {
    digitalWrite(LED_BUILTIN, HIGH);
    delay(500);
    digitalWrite(LED_BUILTIN, LOW);
    delay(500);
    init_oled = true;
  }
}

Using the keystone jack screw terminal and single port enclosure wire in the SHT10 humidity sensor.


sht10

Setup Diagram


Finally connect all your AC devices, Growbed Sensor/Display Module, and Humidity sensors.  Connect your air pump, and oscillating fan directly to the main power.  They are always on and don't need to be controlled, but the power used by these devices are calculated in with your daily power consumption and cost.


Setup

Note: Make sure you use a snubber on relay 3 which is connected to the Radiator Heater which can draw a lot of current when first powered and cause the relay to spark and eventually burn out the relay.


For the Complete Garage Hydroponics Solution please see our other projects

Garage Hydroponics
Hydroponics Deep Water Culture Bucket System
Hydroponics Growbed Sensors/Display Module
Hydroponics Chiller
Hydroponics Water/Nutrient Control
Hydroponics Database Management
Hydroponics Germination Control
Hydroponics CO2 Monitoring
Hydroponics Light Monitoring
Hydroponics pH and DO Monitoring



« Previous  Garage Hydroponics
Hydroponics Deep Water Culture Bucket Sysetm   Next »