Zeven Development

Garage Hydroponics (Using New Z-Wire!)

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. $40.00
1 IO Expander Z-Wire Bundle. $30.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. $22.08
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:

  • 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. Or if you have v1.5 of the IO Expander PCB use 2.2K resistors on R28 an R29.


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 in external credentials.h file, 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.1

*/


/* To enable TELNET_DEBUG modify platform.txt
compiler.cpp.extra_flags=-DTELENT_DEBUG
*/

#include <math.h>
#include <time.h>
#include <Timezone.h>           // 1.10.1
#include <stdlib.h> /* qsort */
#if defined(ESP8266)            // v3.0.2
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <ESP8266HTTPClient.h>
#endif
#if defined(ARDUINO_ARCH_ESP32) // v1.0.6
#include <WiFi.h>
#include <HTTPClient.h>
#endif
#include <WiFiUdp.h>
#include <WiFiClient.h>
#include <ArduinoOTA.h>         // 1.0.9
#include <NTPClient.h>
#ifdef TELNET_DEBUG
#include <TelnetStream.h>       // 1.2.2
#endif
#include "IOExpander.h"         // 1.3
#include "credentials.h"

#define ON                      1
#define OFF                     0
#define on                      ON
#define off                     OFF
#define START                   2
#define STOP                    OFF
#define start                   START
#define stop                    STOP

#define NONE                    0
#define none                    NONE

TimeChangeRule usEDT = {"EDT", Second, Sun, Mar, 2, -240};
TimeChangeRule usEST = {"EST", First, Sun, Nov, 2, -300};
Timezone usEastern(usEDT, usEST);

WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP); //, EST_OFFSET);
WiFiClient wifiClient;        // Must be global or it will cause resets!

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

#define LED_BUILTIN             2

#ifdef TELNET_DEBUG
#define SerialDebug             TelnetStream
#else
#define SerialDebug             Serial1     // Debug goes out on GPIO02
#endif
#define SerialExpander          Serial      // IO Expander connected to the ESP UART

#define FAHRENHEIT
#define ONEWIRE_TO_I2C_MAIN     "z4s6e"     // *** Change 6e
#define RTC_SENSOR              "s4te"
#define I2C_EEPROM              "s4tf"
#define INIT_OLED1              "st13;si;sc;sd"
#define INIT_OLED2              "st133d;si;sc;sd"
#define OLED1_OFF               "st13;sp0"
#define OLED2_OFF               "st133d;sp0"
//#define HUMIDITY_SENSOR_INSIDE  "s6t5"      // DHT22
//#define HUMIDITY_SENSOR_OUTSIDE "s8t1"      // SHT10
// Free port 5-8 by adding Z-Wire to I2C on port 3 and use I2C SHT3x humidity sensors
#define HUMIDITY_SENSOR_INSIDE  "z3s1e;zc0;st3" // SHT3x 100kHz w/ 2.2k pullup *** Change 1e
#define HUMIDITY_SENSOR_OUTSIDE "z3s47;zc0;st3" // SHT31 100kHz w/ 2.2k pullup *** Change 47
#define ALL_RELAYS_OFF          "esffff"

#define VENT_FAN_RELAY          1
#define LIGHTS_RELAY            2
#define HEATER_RELAY            3
#define CHILLER_RELAY           4
#define WATER_PUMP_RELAY        5
#define HEATER_PAD_RELAY        6
#define HUMIDIFIER_RELAY        7

#define OLED_TIME_ON            6
#define OLED_TIME_OFF           0

#define SEC_IN_MIN              60
#define MIN_IN_HOUR             60
#define HOURS_IN_DAY            24
#define SEC_IN_HOUR             (SEC_IN_MIN * MIN_IN_HOUR)
#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_CFS            12          // Cubic Feet per Second
#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 30 minutes
#define VENT_FAN_ON_TIME        ((((ROOM_VOLUME*DUCT_LENGTH)/FOOT_CUBE)/VENT_FAN_CFS)+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 TEMP_DIFF               5           // Temperatire Differential

#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 only half on
#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 HUMIDIFIER_POWER        24          // Humidifier Pump 2.0A * 12V = 24W

#define COST_KWH                10.5311     // First 1000 kWh/month
//#define COST_KWH                11.2214     // 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

#define HUMIDIFIER_ON           60          // 60% Relative Humidity
#define HUMIDIFIER_ON_TIME      2           // 2 seconds of humidifier

#define DEFAULT_TDS_VALUE       488.0

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[] = "i2sc3";  // RJ11 Keystone Crossover Out, T-Connector w/ I2C Bus - OLED Screen/Light Sensor *** Change c3
const char ONEWIRE_TO_I2C_GROW2[] = "i16s1c"; // RJ11 Keystone Crossover Out, T-Connector w/ I2C Bus - OLED Screen/Light Sensor *** Change 1c

const char TEMP1_SENSOR[] =     "t2r92";    // RJ11 Keystone Crossover In for 1-Wire Junction DS18B20 *** Change 92
const char LEVEL1_SELECT[] =    "i2sc3;st1a38"; // *** Change c3
const char LEVEL1_SENSOR[] =    "sr6";      // RJ11 Keystone Crossover for Optical Connector
const char TDS1_SELECT[] =      "i2sc3;st1b"; // *** Change c3
const char TDS1_SENSOR[] =      "sr0";      // RJ11 Plug ADC
#define TDS1_CALIBRATION        (DEFAULT_TDS_VALUE/488.0) // TDS Calibration (Desired/Actual) *** Change
const char WATER1_LEVEL_SENSOR[] = "g10a";
#define WATER1_RELAY            9           // Relay Water Dosing Pump
#define NUTRIENT1_RELAY         NONE        // 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";      // RJ11 Keystone Crossover for Optical Connector
#define TDS2_SELECT             TDS1_SELECT
const char TDS2_SENSOR[] =      "sr1";      // RJ11 Plug ADC
#define TDS2_CALIBRATION        (DEFAULT_TDS_VALUE/488.0) // TDS Calibration (Desired/Actual) *** Change
#define WATER2_RELAY            10          // Relay Water Dosing Pump
#define NUTRIENT2_RELAY         NONE        // Relay Nutrient Dosing Pump
#define CHILLER2_RELAY          16          // Relay Chiller Solenoid

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

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

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[] = "z4sef"; //"i6s08";   // I2C BUS - CO2 Sensor *** Change 08
const char CO2_SENSOR[] =       "st16;zc0";  // SCD30 CO2 Sensor 100kHz
const char INIT_CO2[] =         "sc5,1;si;sc3,2";  // SCD30 Init; Self Calibration, Measurement Interval to 2 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

const char HUMIDIFIER_LEVEL_SENSOR[] = "g9a";

typedef struct {
  bool active;                               // true - ON, false - OFF
  const char* onewire_i2c;                   // 1-Wire to I2C Select
  const char* temp_sensor;                   // Temperature Sensor
  const char* level_select;                  // Level Select
  const char* level_sensor;                  // Level Sensor
  const char* tds_select;                    // TDS Select
  const char* tds_sensor;                    // TDS Sensor
  const char* water_level_sensor;            // Water Level Sensor
  const char* nutrient_level_sensor;         // Nutrient Level Sensor
  uint8_t water_relay;                       // Water Dosing Pump Relay
  uint8_t nutrient_relay;                    // Nutrient Dosing Pump Relay
  uint8_t chiller_relay;                     // Chiller Solenoid Relay
  float tds_calibration;                     // TDS Calibration Factor
} GROWBED_CONFIG_t;

GROWBED_CONFIG_t grow_bed_config_table[] = {
  {off, // Top Left
   ONEWIRE_TO_I2C_GROW1,
   TEMP1_SENSOR,
   LEVEL1_SELECT,
   LEVEL1_SENSOR,
   TDS1_SELECT,
   TDS1_SENSOR,
   WATER1_LEVEL_SENSOR,
   NULL,
   WATER1_RELAY,
   NUTRIENT1_RELAY,
   CHILLER1_RELAY,
   TDS1_CALIBRATION},
  {off, // Top Right
   ONEWIRE_TO_I2C_GROW1,
   TEMP2_SENSOR,
   LEVEL2_SELECT,
   LEVEL2_SENSOR,
   TDS2_SELECT,
   TDS2_SENSOR,
   WATER1_LEVEL_SENSOR,
   NULL,
   WATER2_RELAY,
   NUTRIENT2_RELAY,
   CHILLER2_RELAY,
   TDS2_CALIBRATION},
  {off, // Bottom Left
   ONEWIRE_TO_I2C_GROW2,
   TEMP3_SENSOR,
   LEVEL3_SELECT,
   LEVEL3_SENSOR,
   TDS3_SELECT,
   TDS3_SENSOR,
   WATER1_LEVEL_SENSOR,
   NULL,
   WATER3_RELAY,
   NUTRIENT3_RELAY,
   CHILLER3_RELAY,
   TDS3_CALIBRATION},
  {off, // Bottom Right
   ONEWIRE_TO_I2C_GROW2,
   TEMP4_SENSOR,
   LEVEL4_SELECT,
   LEVEL4_SENSOR,
   TDS4_SELECT,
   TDS4_SENSOR,
   WATER1_LEVEL_SENSOR,
   NULL,
   WATER4_RELAY,
   NUTRIENT4_RELAY,
   CHILLER4_RELAY,
   TDS4_CALIBRATION},
};

typedef struct {
  bool init_oled;
  float water_temp;
  bool water_temp_error;
  bool water_level;
  int16_t water_tds;
  bool water_pump_level;
  uint8_t water_pump;
  uint8_t water_pump_timer;
  bool nutrient_pump_level;
  uint8_t nutrient_pump;
  uint8_t nutrient_mix_time;
  float nutrient_level;
  bool chiller_solenoid;
} GROWBED_t;

GROWBED_t grow_bed_table[sizeof(grow_bed_config_table)/sizeof(GROWBED_CONFIG_t)];

int led = 13;
bool init_oled = true;
bool update_oled = false;
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;

bool ntp_update;
bool ioexpander;

unsigned long _last_rtc = 0;
unsigned long _last_millis = 0;

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

char weekday_text[][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 initWiFi(uint8_t wait) {
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
#ifndef TELNET_DEBUG
  SerialDebug.print("Connecting to WiFi ..");
#endif
  while (WiFi.status() != WL_CONNECTED && wait--) {
#ifndef TELNET_DEBUG
    SerialDebug.print('.');
#endif
    delay(1000);
  }
  //SerialDebug.println();
  //SerialDebug.println(WiFi.localIP());
}

#if !defined(ARDUINO_ARCH_ESP32)
WiFiEventHandler wifiConnectHandler;
WiFiEventHandler wifiDisconnectHandler;

void onWifiConnect(const WiFiEventStationModeGotIP& event) {
  SerialDebug.println("\r\nConnected to Wi-Fi sucessfully.");
  SerialDebug.print("IP address: ");
  SerialDebug.println(WiFi.localIP());
}

void onWifiDisconnect(const WiFiEventStationModeDisconnected& event) {
  SerialDebug.println("\r\nDisconnected from Wi-Fi, trying to connect...");
  WiFi.disconnect();
  WiFi.begin(ssid, password);
}
#endif

void JSONString(String &json, char* json_field, uint16_t* json_time, uint8_t* json_state)
{
  int index = json.indexOf(json_field);
  if (index >= 0) {
     String str_time = json.substring(indexez_plusstrlen(json_field)ez_plus2);
     str_time.replace('.',NULL);
     *json_time = str_time.toInt();
     *json_state = (json.indexOf("true") > 0) ? true : false;
  }
}

void HttpPost(const char *url, String &post_data)
{
  HTTPClient http;
  //http.begin(url);
  http.begin(wifiClient, 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);

      JSONString(request, "OVERRIDE_LIGHTS_TIME", &OVERRIDE_LIGHTS_TIME, &OVERRIDE_LIGHTS);
      JSONString(request, "OVERRIDE_VENT_FAN_TIME", &OVERRIDE_VENT_FAN_TIME, &OVERRIDE_VENT_FAN);
     
      index = next;
    } while (index >= 0);
  }

  http.end();                             // Close connection
}

void AddPower(uint32_t watts, uint16_t sec)
{
  if (!sec || sec > SEC_IN_MIN) sec = SEC_IN_MIN;
  nvram.energy_usage[nvram.energy_wday] += (watts * sec * 100) / SEC_IN_HOUR;
  power += watts;
}

uint16_t ControlPower(uint8_t device, uint8_t relay, uint32_t power, uint16_t sec)
{
  //char cmd[10];
  uint16_t bits = 0;
  //sprintf(cmd, "e%d%c", relay, (device == on) ? 'o' : 'f');
  //SerialCmdDone(cmd);  
  if (device) {
    bits |= 1 << (relay-1);
    AddPower(power, sec);
    // Resend relay cmd again incase the relay board resets due to a large power drop due to heater or compressor.
    //delay(100);
    //SerialCmdDone(cmd);    
  }

  return bits;
}

void ControlRelay(uint8_t device, uint8_t relay, uint32_t power, uint16_t sec)
{
  char cmd[10];

  sprintf(cmd, "e%d%c", relay, (device) ? 'o' : 'f');
  SerialCmdDone(cmd);
  ControlPower(device, relay, power, sec);
}
 
void setup() {
  //ESP.wdtDisable();  

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

#ifdef TELNET_DEBUG
  initWiFi(10);

  TelnetStream.begin();
  TelnetStream.println("\r\nGarage Hydroponics");
#else
#ifdef SERIAL_DEBUG
#if defined(ARDUINO_ARCH_ESP32)
  // !!! Debug output goes to TXD2
  SerialDebug.begin(115200, SERIAL_8N1, 16, 17);
#else
  // !!! Debug output goes to GPIO02 !!!
  SerialDebug.begin(115200);

  //Register event handlers
  wifiConnectHandler = WiFi.onStationModeGotIP(onWifiConnect);
  wifiDisconnectHandler = WiFi.onStationModeDisconnected(onWifiDisconnect);
#endif // ARDUINO_ARCH_ESP32
#endif // SERIAL_DEBUG
  SerialDebug.println("\r\nGarage Hydroponics");
  swSerialEcho = &SerialDebug;

  initWiFi(10);
#endif // TELNET_DEBUG

  WiFi.setAutoReconnect(true);
  WiFi.persistent(true);
 
  ArduinoOTA.setHostname("GarageHydroponics");
  ArduinoOTA.setPassword("password");

  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();
  ntp_update = timeClient.update();

  SerialExpander.begin(115200);
  Serial.println();                       // If there is noise on the TX line  
  delay(1000);                            // Delay 1 sec for IO Expander title

  while (Serial.available()) Serial.read(); // Flush RX buffer
  Serial.println();
  ioexpander = SerialReadUntilDone();
 
  //SerialCmdDone("eb1");                   // Initialize for one x16 relay board

  // Initialize growbed structure
  memset(grow_bed_table, 0, sizeof(grow_bed_table));
  GROWBED_t* grow_bed = grow_bed_table;
  for (int i = 0; i < sizeof(grow_bed_table) / sizeof(GROWBED_t); i++) {
    grow_bed->init_oled = true;
    grow_bed->nutrient_level = DEFAULT_TDS_VALUE;
    grow_bed++;
  }
}

  void loop() {
  static HS inside, outside;
  static bool vent_fan = off;
  static bool lights = off;
  static bool heater = off;
  static bool humidifier = off;
  static int8_t heater_pad = off;
  static int8_t chiller = off;
  bool water_pump;
  static tm rtc;
  static tm clk;
  tm trtc;
  time_t rtc_time;
  //time_t clk_time;
  static time_t vent_fan_next_time;
  static uint16_t vent_fan_on_time;
  static uint8_t last_min = -1;
  static uint8_t last_sec = -1;
  bool error_rtc;
  static bool read_nvram = true;
  static bool clear_nvram = false;
  static bool init_relays = true;
  static 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;
  static int color_temp, lux;
  char error[40];
  uint16_t clk_day_min;
  uint8_t i, wday;
  GROWBED_CONFIG_t* grow_bed_config;
  GROWBED_CONFIG_t* left_grow_bed_config;
  GROWBED_CONFIG_t* right_grow_bed_config;
  GROWBED_t* grow_bed;
  GROWBED_t* left_grow_bed;
  GROWBED_t* right_grow_bed;
  signed long level;
  float voltage, vref;
  uint8_t t;
  String post_data;
  static float co2, co2_temp, co2_relative;
  static uint8_t co2_samples = 0;
  static float co2_data[CO2_SAMPLES_IN_MIN];
  static float germination_temp;
  bool germination_active = false;
  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;
  static uint16_t humidifier_on_time = 0;
  static bool humidifier_level;
  uint16_t relays;

  ArduinoOTA.handle();

#ifdef TELNET_DEBUG
  if (TelnetControl()) return;
#endif
  if (SerialDebugControl()) return;

  if (ntp_update) rtc_time = timeClient.getEpochTime();
  else rtc_time = _last_rtc ez_plus ((millis() - _last_millis) / 1000);    
  gmtime_r(&rtc_time, &rtc);
 
  if (ioexpander) {
    if (init_relays) {
      SerialCmdDone("eb1"); // Initialize for one x16 relay board
      SerialCmdDone(ALL_RELAYS_OFF);
      init_relays = false;
    }

    if (init_rtc) {
      if (SerialCmdNoError(ONEWIRE_TO_I2C_MAIN) &&  
          SerialCmdDone(RTC_SENSOR)) {
        if (ntp_update) {    
          SerialWriteTime(&rtc);
        }
        else
        {
          error_rtc = !SerialReadTime(&rtc);        
          if (!error_rtc) {
            trtc = rtc;
            rtc_time = mktime(&trtc);
            _last_millis = millis();
            _last_rtc = rtc_time;  
          }
        }
        init_rtc = false;
      }
    }

    if (read_nvram) {
      if (SerialCmdNoError(ONEWIRE_TO_I2C_MAIN) &&
          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;
        }
      }
    }
  }

  time_t local_time = usEastern.toLocal(rtc_time);
  gmtime_r(&local_time, &clk);

  SerialDebug.println(clk.tm_sec);
 
  // Process every second
  if (clk.tm_sec != last_sec) {
    if (vent_fan_on_time) {
      if (!--vent_fan_on_time) {
        vent_fan = off;
        ControlRelay(vent_fan, VENT_FAN_RELAY, VENT_FAN_POWER, 0);
        update_oled = true;
      }
    }
   
    if (humidifier == START) {
      humidifier = on;
      ControlRelay(humidifier, HUMIDIFIER_RELAY, HUMIDIFIER_POWER, humidifier_on_time);
    }
    else {
      if (humidifier_on_time) {
        if (!--humidifier_on_time) {
          humidifier = off;
          ControlRelay(humidifier, HUMIDIFIER_RELAY, HUMIDIFIER_POWER, 0);
          update_oled = true;
        }          
      }
    }
   
    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();
        }      
      }
    }

    last_sec = clk.tm_sec;        
  }      

  // Process every minute
  if (clk.tm_min != last_min)
  {
    while (Serial.available()) Serial.read(); // Flush RX buffer
    Serial.println();
    if (ioexpander = SerialReadUntilDone()) {

      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 = on;
        else lights = off;
        // 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 = off;
      }

      // Check air ventilation
      if (OVERRIDE_VENT_FAN_TIME) {
        vent_fan_on_time = OVERRIDE_VENT_FAN * SEC_IN_MIN;
        vent_fan = on;
      }
      else {
        if (vent_fan_next_time <= rtc_time) {
          vent_fan_on_time = VENT_FAN_ON_TIME;
          vent_fan = on;
        }
        if (!vent_fan_on_time) {
          vent_fan = off;            
          if (lights) {
            if ((C2F(inside.temp) < MIN_DAY_TEMP && C2F(outside.temp) - C2F(inside.temp) >= TEMP_DIFF) ||
                (C2F(inside.temp) > MAX_DAY_TEMP && C2F(inside.temp) - C2F(outside.temp) >= TEMP_DIFF))
              vent_fan = on;
          }
          else {
            if ((C2F(inside.temp) < MIN_NIGHT_TEMP && C2F(outside.temp) - C2F(inside.temp) >= TEMP_DIFF) ||
                (C2F(inside.temp) > MAX_NIGHT_TEMP && C2F(inside.temp) - C2F(outside.temp) >= TEMP_DIFF))
              vent_fan = on;
          }
        }
      }
      if (vent_fan == on) vent_fan_next_time = rtc_time ez_plus (AIR_EXCHANGE_TIME * SEC_IN_MIN);

      // 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 = off;
        }
        else {
          if (C2F(inside.temp) <= HEATER_ON_DAY_TEMP) heater = on;
        }
      }
      else {
        if (heater) {
          if (C2F(inside.temp) >= HEATER_OFF_NIGHT_TEMP) heater = off;
        }
        else {
          if (C2F(inside.temp) <= HEATER_ON_NIGHT_TEMP) heater = on;
        }
      }

      // Check Humidifier
      humidifier_level = true;
      if (HUMIDIFIER_LEVEL_SENSOR) {
          SerialCmd(HUMIDIFIER_LEVEL_SENSOR);
          if (SerialReadFloat(&voltage)) {
            if (voltage == 0) humidifier_level = false;
            else humidifier_level = (voltage > 3.0) ? false : true;
            //humidifier_level = (level == 0);  
          }
          SerialReadUntilDone();
      }

      if (humidifier) {
        if (inside.relative >= MAX_HUMIDITY) {
          humidifier_on_time = 0;
          humidifier = off;
        }
      }
      else {
        if (humidifier_level && inside.relative <= HUMIDIFIER_ON) {
          humidifier_on_time = HUMIDIFIER_ON_TIME;
          humidifier = start;
        }
      }

      // 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 = off;
                chiller_recovery_time = 0;
              }
            }
            else {
              if (C2F(chiller_temp) >= CHILLER_ON_WATER_TEMP) {
                chiller_cycle = CHILLER_CYCLE_TIME;
                chiller = on;
              }
            }
          }
        }
        SerialReadUntilDone();
      }
      else {
        chiller_temp = ERROR_NO_ROM;
        chiller = off;
      }

      // 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 = off;
          }
          else {
            if (C2F(germination_temp) < GERMINATION_ON_TEMP) heater_pad = on;
          }
        }
        else heater_pad = off;
        SerialReadUntilDone();
      }
      else {
        germination_temp = ERROR_NO_ROM;
        heater_pad = off;
      }

      // 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);
        }
      }

      relays = 0;
      power = ALWAYS_ON_POWER;

      // Update Grow Beds
      water_pump = false;
      grow_bed_config = grow_bed_config_table;
      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_config->level_select || SerialCmdNoError(grow_bed_config->level_select)) {
          //if (grow_bed_config->level_select) SerialCmdDone(grow_bed_config->level_select);
          SerialCmd(grow_bed_config->level_sensor);
          if (SerialReadInt(&level)) {
            grow_bed->water_level = (level == 0);
          }
          SerialReadUntilDone();
        }

        // Check the water temperature
        SerialCmd(grow_bed_config->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_config->tds_sensor) {
          if (grow_bed_config->tds_select) SerialCmdDone(grow_bed_config->tds_select);
          SerialCmd(grow_bed_config->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_config->tds_calibration;
          }
          SerialReadUntilDone();
        }

        // Check water and nutrient pump levels
        grow_bed->water_pump_level = true;
        if (grow_bed_config->water_level_sensor) {
          // Don't read the water level sensor if it's the same as the previous
          if (i && grow_bed_config->water_level_sensor == (grow_bed_config-1)->water_level_sensor)
            grow_bed->water_pump_level = (grow_bed-1)->water_pump_level;
          else {
            SerialCmd(grow_bed_config->water_level_sensor);
            if (SerialReadFloat(&voltage)) {
              if (voltage == 0) grow_bed->water_pump_level = false;
              else grow_bed->water_pump_level = (voltage > 3.0) ? false : true;
              //grow_bed->water_pump_level = (level == 0);  
            }
            SerialReadUntilDone();
          }
        }
        grow_bed->nutrient_pump_level = true;
        if (grow_bed_config->nutrient_level_sensor) {
          // Don't read the water level sensor if it's the same as the previous
          if (i && grow_bed_config->nutrient_level_sensor == (grow_bed_config-1)->nutrient_level_sensor)
            grow_bed->nutrient_pump_level = (grow_bed-1)->nutrient_pump_level;
          else {  
            SerialCmd(grow_bed_config->nutrient_level_sensor);
            if (SerialReadFloat(&voltage)) {
              if (voltage == 0) grow_bed->nutrient_pump_level = false;
              else grow_bed->nutrient_pump_level = (voltage > 3.0) ? false : true;
              //grow_bed->nutrient_pump_level = (level == 0);
            }
            SerialReadUntilDone();
          }
        }

        // Check dosing pumps.  Allow for a one minute mixing cycle between nutrient pumps.
        if (!grow_bed_config->active || grow_bed->water_level || grow_bed->nutrient_mix_time ||
            !grow_bed->water_pump_level) {
          grow_bed->water_pump = false;
          grow_bed->water_pump_timer = 0;
          if (grow_bed->nutrient_mix_time) {
            grow_bed->nutrient_mix_time--;
            if (!grow_bed->nutrient_pump_level) grow_bed->nutrient_mix_time = 0;
          }
          grow_bed->nutrient_pump = (grow_bed->nutrient_mix_time == 0) ? false : true;
        }
        else {
          grow_bed->nutrient_pump = (grow_bed_config->nutrient_relay &&
                                      grow_bed->nutrient_pump_level &&
                                      grow_bed->water_tds < grow_bed->nutrient_level) ? true : false;
          if (grow_bed->nutrient_pump) grow_bed->nutrient_mix_time = NUTRIENT_MIX_TIME;
          else {
            if (grow_bed_config->water_relay) {
              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_config->water_relay) {
          //Serial.print("e");
          //Serial.print(grow_bed_config->water_relay);
          //Serial.print(grow_bed->water_pump ? "o" : "f");
          relays |= ControlPower(grow_bed->water_pump, grow_bed_config->water_relay, DOSING_PUMP_POWER, 0);
        }
        if (grow_bed_config->nutrient_relay) {
          //Serial.print(";e");
          //Serial.print(grow_bed_config->nutrient_relay);
          //Serial.print(grow_bed->nutrient_pump ? "o" : "f");
          relays |= ControlPower(grow_bed->nutrient_pump, grow_bed_config->nutrient_relay, DOSING_PUMP_POWER, 0);
        }
        Serial.println();
        SerialReadUntilDone();
       
        //if (grow_bed->water_pump) AddPower(DOSING_PUMP_POWER, 0);
        //if (grow_bed->nutrient_pump) AddPower(DOSING_PUMP_POWER, 0);

        // Check chiller pumps
        if (grow_bed_config->chiller_relay) {
          if (grow_bed_config->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_config->chiller_relay);
          //SerialCmdDone((grow_bed->chiller_solenoid) ? "o" : "f");
          relays |= ControlPower(grow_bed->chiller_solenoid, grow_bed_config->chiller_relay, CHILLER_SOLENOID_POWER, 0);
          if (grow_bed->chiller_solenoid) {
            water_pump = true;
            //AddPower(CHILLER_SOLENOID_POWER, 0);
            //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_config++;
        grow_bed++;
      }

      SerialDebug.println("Energy");

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

      // Turn on/off the lights, fan, heater, heater pad, chiller, water pump, and humdifier
     
      relays |= ControlPower(vent_fan, VENT_FAN_RELAY, VENT_FAN_POWER, vent_fan_on_time);
      relays |= ControlPower(lights, LIGHTS_RELAY, LIGHTS_POWER, 0);
      //heater = false;
      relays |= ControlPower(heater, HEATER_RELAY, HEATER_POWER, 0);
      relays |= ControlPower(heater_pad, HEATER_PAD_RELAY, HEATER_PAD_POWER, 0);
      relays |= ControlPower(chiller, CHILLER_RELAY, CHILLER_POWER, 0);
      relays |= ControlPower(water_pump, WATER_PUMP_RELAY, WATER_PUMP_POWER, 0);
      relays |= ControlPower(humidifier, HUMIDIFIER_RELAY, HUMIDIFIER_POWER, humidifier_on_time);
      relays = __builtin_bswap16(~relays);
      SerialWriteRelayExpander((uint8_t*)&relays, 2);
     
      delay(100);      

      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);

      update_oled = true;

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

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

      //yield();

      if (WiFi.status() == WL_CONNECTED) {      
  #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));
        }
      }
    }
    else SerialDebug.println("IO Expander not found!");
   
    last_min = clk.tm_min;
  }

  if (ioexpander && update_oled) {
    // Display main status
    if (SerialCmdNoError(ONEWIRE_TO_I2C_MAIN)) {
      if ((OLED_TIME_OFF > OLED_TIME_ON && clk.tm_hour >= OLED_TIME_ON && clk.tm_hour < OLED_TIME_OFF) ||
          (OLED_TIME_OFF <= OLED_TIME_ON && !(clk.tm_hour >= OLED_TIME_OFF && clk.tm_hour < OLED_TIME_ON))) {
        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.1");
          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_text[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("\"");
            }
          }
          Serial.print(";sf0;sa0");
          if (!humidifier_level) Serial.print(";sd0,0,\"HUM\"");
          else if (lights) Serial.print(";sd0,0,\"LT\"");
          if (chiller) Serial.print(";sd0,14,\"CHILL\"");
          Serial.println(";sd");
          SerialReadUntilDone();
        }
      }
      else {
        if (!init_oled) {
          SerialCmdDone(OLED1_OFF);
          SerialCmdDone(OLED2_OFF);
          init_oled = true;
        }
      }
    }
    else init_oled = true;

    // Display Grow Beds
    for (i = 0; i < sizeof(grow_bed_table) / sizeof(GROWBED_t); i ez_plus= 2) {
      left_grow_bed_config = &grow_bed_config_table[i];
      right_grow_bed_config = &grow_bed_config_table[iez_plus1];
      left_grow_bed = &grow_bed_table[i];
      right_grow_bed = &grow_bed_table[iez_plus1];
      if (SerialCmdNoError(left_grow_bed_config->onewire_i2c)) {
        if ((OLED_TIME_OFF > OLED_TIME_ON && clk.tm_hour >= OLED_TIME_ON && clk.tm_hour < OLED_TIME_OFF) ||
            (OLED_TIME_OFF <= OLED_TIME_ON && !(clk.tm_hour >= OLED_TIME_OFF && clk.tm_hour < OLED_TIME_ON))) {
          if (left_grow_bed->init_oled) {
            if (SerialCmdNoError(INIT_OLED1))
              left_grow_bed->init_oled = false;
          }
          if (!left_grow_bed->init_oled) {
            SerialCmdDone("st13;sc;sf1;sa0;sd0,12,248,\""
#ifdef FAHRENHEIT
                          "F"
#else
                          "C"
#endif
                          "\"");
            if (left_grow_bed_config->tds_sensor || right_grow_bed_config->tds_sensor) SerialCmdDone("sf0;sd0,32,\"ppm\"");
            SerialPrint("sf1;sa1;sd70,12,\"", C2F(left_grow_bed->water_temp), 1, left_grow_bed->water_temp_error);
            if (left_grow_bed_config->tds_sensor) SerialPrint("\";sd70,30,\"", left_grow_bed->water_tds, 0, false);
            SerialPrint("\";sd125,12,\"", C2F(right_grow_bed->water_temp), 1, right_grow_bed->water_temp_error);
            if (right_grow_bed_config->tds_sensor) SerialPrint("\";sd125,30,\"", right_grow_bed->water_tds, 0, false);
            Serial.print("\";sf0;sa0;sd0,0,\"");
            if (!left_grow_bed_config->active) Serial.print("OFF");
            else if (left_grow_bed->water_pump || left_grow_bed->nutrient_pump) Serial.print("PUMP");
            else if (!left_grow_bed->water_level) Serial.print("LOW");
            else if (left_grow_bed->chiller_solenoid) Serial.print("CHILL");
            else Serial.print(" ");
            Serial.print("\";sf0;sa1;sd126,0,\"");
            if (!right_grow_bed_config->active) Serial.print("OFF");
            else if (right_grow_bed->water_pump || right_grow_bed->nutrient_pump) Serial.print("PUMP");
            else if (!right_grow_bed->water_level) Serial.print("LOW");
            else if (!right_grow_bed->water_pump_level || !right_grow_bed->nutrient_pump_level) Serial.print("REFIL");
            else if (right_grow_bed->chiller_solenoid) Serial.print("CHILL");
            else Serial.print(" ");
            Serial.println("\"");
            SerialReadUntilDone();

            if (C2F(left_grow_bed->water_temp) < MIN_WATER_TEMP)
              SerialCmdDone("sh29,11,44;sh29,29,44;sv29,12,17;sv72,12,17");
            else if (C2F(left_grow_bed->water_temp) > MAX_WATER_TEMP)
              SerialCmdDone("so2;sc29,11,44,19;so1");
            if (C2F(right_grow_bed->water_temp) < MIN_WATER_TEMP)
              SerialCmdDone("sh85,11,44;sh85,29,44;sv85,12,17;sv127,12,17");
            else if (C2F(right_grow_bed->water_temp) > MAX_WATER_TEMP)
              SerialCmdDone("so2;sc85,11,44,19;so1");
            SerialCmdDone("sd");
          }
        }
        else {
          if (!left_grow_bed->init_oled) {
            SerialCmdDone("st13;sp0");
            left_grow_bed->init_oled = true;
          }
        }
      }
      else left_grow_bed->init_oled = true;
    }

    update_oled = false;
  }

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

  if (ioexpander) delay(1000);
  else {
    digitalWrite(LED_BUILTIN, HIGH);
    delay(500);
    digitalWrite(LED_BUILTIN, LOW);
    delay(500);
  }
}

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 »