Skip to content

Instantly share code, notes, and snippets.

@ItKindaWorks
Created September 11, 2017 14:34
Show Gist options
  • Save ItKindaWorks/d94f6031fb377df8ddde56468c50833d to your computer and use it in GitHub Desktop.
Save ItKindaWorks/d94f6031fb377df8ddde56468c50833d to your computer and use it in GitHub Desktop.
An ESP8266 program to control valves for home irrigation
/*
watering.ino
Copyright (c) 2016 ItKindaWorks All right reserved.
github.com/ItKindaWorks
watering.ino is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
watering.ino is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with watering.ino. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ESPHelper.h"
#include "Metro.h"
#include <TimeLib.h>
#include <WiFiUdp.h>
#include <Bounce2.h>
//setup macros for time
#define SECOND 1000L
#define MINUTE SECOND * 60L
#define HOUR MINUTE * 60L
//pin defs
const int relay1 = 13;
const int relay2 = 12;
const int relay3 = 14;
const int button1 = 5;
const int button2 = 4;
//debouncers for each button
Bounce buttonDebouncer1 = Bounce();
Bounce buttonDebouncer2 = Bounce();
//consts for the timers for a cycle and section
const unsigned long SECTION_TIME = 15L * MINUTE;
const unsigned long CYCLE_TIME = 3 * SECTION_TIME;
const int TRIGGER_HOUR = 6;
const int TRIGGER_MIN = 30;
const int TIMEZONE = -4;
//ESPHelper vars
const char* HOSTNAME = "ESP-Water";
const char* OTA_PASS = "OTA_PASSWORD";
const char* CALLBACK_TOPIC = "/home/watering";
netInfo homeNet = { .mqttHost = "YOUR MQTT-IP", //can be blank if not using MQTT
.mqttUser = "YOUR MQTT USERNAME", //can be blank
.mqttPass = "YOUR MQTT PASSWORD", //can be blank
.mqttPort = 1883, //default port for MQTT is 1883 - only chance if needed.
.ssid = "YOUR SSID",
.pass = "YOUR NETWORK PASS"};
ESPHelper myESP(&homeNet);
//NTP setup vars
static const char ntpServerName[] = "us.pool.ntp.org";
const int timeZone = TIMEZONE;
WiFiUDP Udp;
unsigned int localPort = 8888; // local port to listen for UDP packets
time_t getNtpTime();
void sendNTPpacket(IPAddress &address);
//runCycle & hasStarted are variables that keep track of whether or not a cycle should be running.
//runCycle tells the system that a cycle is running and hasStarted tells the system whether or not it has triggered
//the start to a cycle. Basically runCycle is the overall tracker and hasStarted just keeps track to make sure that we dont
//continually "start" a cycle
bool runCycle = false;
bool hasStarted = false;
//mode determines which section is being watered/which valve is active.
int mode = 0;
//allows user to send a command and the system will skip the next watering cycle
bool skipNext = false;
//testRunning is flagged when the system is running a test
bool testRunning = false;
//button states
bool buttonState1 = false;
bool buttonState2 = false;
//whether the NTP has been initialized
bool initDone = false;
//timers for various watering functions
Metro cycleTimer = Metro(CYCLE_TIME);
Metro sectionTimer = Metro(SECTION_TIME);
Metro testTimer = Metro(30 * SECOND);
void setup() {
//setup the relay pins
pinMode(relay1, OUTPUT);
pinMode(relay2, OUTPUT);
pinMode(relay3, OUTPUT);
delay(100);
setValve(0); //init to valves off
//setup the button pins and attach to debouncers
pinMode(button1, INPUT);
pinMode(button2, INPUT);
buttonDebouncer1.attach(button1);
buttonDebouncer1.interval(10);
buttonDebouncer2.attach(button2);
buttonDebouncer2.interval(10);
//setup ESPHelper
myESP.OTA_enable();
myESP.OTA_setPassword(OTA_PASS);
myESP.OTA_setHostnameWithVersion(HOSTNAME);
myESP.enableHeartbeat(2);
myESP.setHopping(false);
myESP.addSubscription(CALLBACK_TOPIC);
myESP.setCallback(callback);
myESP.begin(); //start ESPHelper
//setup NTP
Udp.begin(localPort);
setSyncProvider(getNtpTime);
setSyncInterval(300);
}
void loop(){
if(myESP.loop() >= WIFI_ONLY){
//once we have a WiFi connection trigger some extra setup
if(!initDone){
delay(100);
//setup the NTP connection and get the current time
setupNTP();
delay(200);
//only set initDone to true if the time is set
if(timeStatus() != timeNotSet){
initDone = true;
postTimeStamp("Watering System Started");
}
}
if(initDone){
//refresh the button states
buttonDebouncer1.update();
buttonDebouncer2.update();
//get the current time
time_t t = now();
//button1 triggers the start of a cycle
if(buttonDebouncer1.fell() && !runCycle){
postTimeStamp("Button initiated cycle");
runCycle = true;
cycleTimer.reset();
sectionTimer.reset();
}
//button2 triggers a valve test
else if(buttonDebouncer2.fell()){
postTimeStamp("Watering Test Started");
testRunning = true;
testTimer.reset();
}
//trigger the start of a cycle if needed based on time
if(hour(t) == TRIGGER_HOUR && minute(t) == TRIGGER_MIN && !runCycle && !skipNext){
postTimeStamp("Time initiated cycle");
runCycle = true;
cycleTimer.reset();
sectionTimer.reset();
}
else if(hour(t) == TRIGGER_HOUR && minute(t) == TRIGGER_MIN + 1 && skipNext){
postTimeStamp("Cycle Skipped");
skipNext = false;
}
//cycle timer resets/ends a cycle after a set period of time
if(cycleTimer.check()){runCycle = false;}
if(testRunning){runTest();}
else{cycleHandler();}
}
}
delay(20);
}
//MQTT callback
void callback(char* topic, byte* payload, unsigned int length) {
//Convert topic to String to make it easier to work with
//Also fix the payload by null terminating it and also convert that
String topicStr = topic;
char newPayload[256];
memcpy(newPayload, payload, length);
newPayload[length] = '\0';
String payloadStr = newPayload;
if(topicStr.equals(CALLBACK_TOPIC)){
if(newPayload[0] == '1'){
skipNext = true;
postTimeStamp("Skipping next watering cycle");
}
else if(newPayload[0] == '0'){
skipNext = false;
postTimeStamp("Not skipping next watering cycle");
}
}
}
void cycleHandler(){
//if we want to run but have not started yet
if(runCycle && !hasStarted){
hasStarted = true;
mode = 1;
sectionTimer.reset();
postTimeStamp("Started Cycle");\
return;
}
//if we dont want to run but we are currently running
else if(!runCycle && hasStarted){
hasStarted = false;
setValve(0);
postTimeStamp("Ended Cycle");
return;
}
//manages valves while a watering cycle is running
if(runCycle){
if(sectionTimer.check()){
mode++;
postTimeStamp("Mode Change");
}
if(mode < 4){
setValve(mode);
}
else{
hasStarted = false;
setValve(0);
postTimeStamp("Ended Cycle via mode change");
mode = 0;
runCycle = false;
}
}
}
void runTest (){
static int testState = 1;
if(testTimer.check()){
testState++;
}
if(testState < 4){setValve(testState);}
else{
postTimeStamp("Watering Test Ended");
testRunning = false;
setValve(0);
testState = 1;
}
}
void setValve(int valveNum){
static int lastSet = -1;
if(valveNum == 1 && lastSet != valveNum){
postTimeStamp("Valve 1 Open");
digitalWrite(relay1, HIGH);
delay(100);
digitalWrite(relay2, LOW);
delay(100);
digitalWrite(relay3, LOW);
lastSet = valveNum;
}
else if(valveNum == 2 && lastSet != valveNum){
postTimeStamp("Valve 2 Open");
digitalWrite(relay1, LOW);
delay(100);
digitalWrite(relay2, HIGH);
delay(100);
digitalWrite(relay3, LOW);
lastSet = valveNum;
}
else if(valveNum == 3 && lastSet != valveNum){
postTimeStamp("Valve 3 Open");
digitalWrite(relay1, LOW);
delay(100);
digitalWrite(relay2, LOW);
delay(100);
digitalWrite(relay3, HIGH);
lastSet = valveNum;
}
else if(lastSet != valveNum){
postTimeStamp("All Valves Closed");
digitalWrite(relay1, LOW);
digitalWrite(relay2, LOW);
digitalWrite(relay3, LOW);
lastSet = valveNum;
}
}
//take a char* and use it as a message with an appended timestamp
void postTimeStamp(const char* text){
char timeStamp[30];
createTimeString(timeStamp, 30);
String pubString = String(HOSTNAME);
pubString += " : ";
pubString += text;
pubString += " - ";
pubString += timeStamp;
//conver the String into a char*
char message[128];
pubString.toCharArray(message, 128);
myESP.publish("/home/watering/status", message, true);
}
//create a timestamp string
void createTimeString(char* buf, int length){
time_t t = now();
String timeString = String(hour(t));
timeString += ":";
timeString += minute(t);
timeString += ":";
timeString += second(t);
timeString += " ";
timeString += month(t);
timeString += "/";
timeString += day(t);
timeString += "/";
timeString += year(t);
timeString.toCharArray(buf, length);
}
/*-------- NTP code ----------*/
void setupNTP(){
delay(200);
Udp.begin(localPort);
setSyncProvider(getNtpTime);
setSyncInterval(300);
}
const int NTP_PACKET_SIZE = 48; // NTP time is in the first 48 bytes of message
byte packetBuffer[NTP_PACKET_SIZE]; //buffer to hold incoming & outgoing packets
time_t getNtpTime(){
IPAddress ntpServerIP; // NTP server's ip address
while (Udp.parsePacket() > 0) ; // discard any previously received packets
// get a random server from the pool
WiFi.hostByName(ntpServerName, ntpServerIP);
sendNTPpacket(ntpServerIP);
uint32_t beginWait = millis();
while (millis() - beginWait < 1500) {
int size = Udp.parsePacket();
if (size >= NTP_PACKET_SIZE) {
Udp.read(packetBuffer, NTP_PACKET_SIZE); // read packet into the buffer
unsigned long secsSince1900;
// convert four bytes starting at location 40 to a long integer
secsSince1900 = (unsigned long)packetBuffer[40] << 24;
secsSince1900 |= (unsigned long)packetBuffer[41] << 16;
secsSince1900 |= (unsigned long)packetBuffer[42] << 8;
secsSince1900 |= (unsigned long)packetBuffer[43];
return secsSince1900 - 2208988800UL + timeZone * SECS_PER_HOUR;
}
}
return 0; // return 0 if unable to get the time
}
// send an NTP request to the time server at the given address
void sendNTPpacket(IPAddress &address)
{
// set all bytes in the buffer to 0
memset(packetBuffer, 0, NTP_PACKET_SIZE);
// Initialize values needed to form NTP request
// (see URL above for details on the packets)
packetBuffer[0] = 0b11100011; // LI, Version, Mode
packetBuffer[1] = 0; // Stratum, or type of clock
packetBuffer[2] = 6; // Polling Interval
packetBuffer[3] = 0xEC; // Peer Clock Precision
// 8 bytes of zero for Root Delay & Root Dispersion
packetBuffer[12] = 49;
packetBuffer[13] = 0x4E;
packetBuffer[14] = 49;
packetBuffer[15] = 52;
// all NTP fields have been given values, now
// you can send a packet requesting a timestamp:
Udp.beginPacket(address, 123); //NTP requests are to port 123
Udp.write(packetBuffer, NTP_PACKET_SIZE);
Udp.endPacket();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment