Bobby Leonard
Published © GPL3+

Python/MicroPython IoT Framework Example - Auto Irrigation

A Py/mPy auto irrigation system with push notifications to Android, and rainfall prediction with DarkSky API for more efficient water usage.

AdvancedFull instructions provided3 hours15,675
Python/MicroPython IoT Framework Example - Auto Irrigation

Things used in this project

Hardware components

Raspberry Pi 3 Model B
Raspberry Pi 3 Model B
×1
NodeMCU ESP8266 Breakout Board
NodeMCU ESP8266 Breakout Board
×1
Wemos D1 Mini
Espressif Wemos D1 Mini
×1
DFRobot capacitive soil moisture sensor
×1
Ultrasonic Sensor - HC-SR04 (Generic)
Ultrasonic Sensor - HC-SR04 (Generic)
×1
Relay Module (Generic)
×1
Resistor 1k ohm
Resistor 1k ohm
×1
DHT11 Temperature & Humidity Sensor (4 pins)
DHT11 Temperature & Humidity Sensor (4 pins)
×1
Photo resistor
Photo resistor
×1
Jumper wires (generic)
Jumper wires (generic)
×1
Breadboard (generic)
Breadboard (generic)
×1
Android device
Android device
×1

Software apps and online services

Slack
Slack
Google Sheets
Google Sheets
Darksky Weather API

Story

Read more

Schematics

NodeMCU Temp/Humidity/Light

DHT11 and light level

NodeMCU Soil Moisture

The setup for DFRobot capacitive soil moisture sensor.

WemosD1 Pump Controller

The setup for the pump controller board

Code

RPi3PythonServer.py

Python
This is the Flask Server, run on anything with Python3.
I've marked the points you need to edit if you want to add a new device or functionality.
Dont forget to add your Slack token on line 128
from slacker import Slacker
import gspread
from oauth2client.service_account import ServiceAccountCredentials
from flask import Flask, jsonify, abort, make_response, request, url_for
import forecastio

# Note: if you see '# Add new device here(X)'
# This is where you copy the syntax directly above to add a new device
# There are 6 points in total.

# Darksky weather info
# Go to https://darksky.net/dev and register
# Google your lat and long
api_key = "<API KEY>"
lat = 51.76524
lng = -10.16642

scope = ['https://spreadsheets.google.com/feeds',
         'https://www.googleapis.com/auth/drive']

def gAuth():
        global credentials, gc, wemosd1worksheet,
        nodemcu_1worksheet, nodemcu_2worksheet
        # Add new device here(1)
        
        credentials = ServiceAccountCredentials.from_json_keyfile_name('clientsecret.json', scope)
        
        # Authorise with Google
        gc = gspread.authorize(credentials) 
        # Open the wemosd1 worksheet
        wemosd1worksheet = gc.open("gsheet").get_worksheet(0) 
        # Open the nodemcu_1 worksheet
        nodemcu_1worksheet = gc.open("gsheet").get_worksheet(1) 
        # Open the nodemcu_2 worksheet
        nodemcu_2worksheet = gc.open("gsheet").get_worksheet(2)
        
        # Add new device here(2)


gAuth()

# Delete Old Values
wemosd1worksheet.clear() 
nodemcu_1worksheet.clear()
nodemcu_2worksheet.clear()

# Add new device here(3)

# Add column names to empty spreadsheet
wemosd1worksheet.append_row(['Time', 'Soil Moisture %'], value_input_option='RAW') 
nodemcu_1worksheet.append_row(['Time', 'Light %', 'Temp *C', 'Humid %RH'], value_input_option='RAW')
nodemcu_2worksheet.append_row(['Time', 'Tank Water Level'], value_input_option='RAW')

# Add new device here(4)


app = Flask(__name__)

# Declare variables that you want to pass between devices.
smlevel = 0

# Think of these as containers for mPy sensor data.
wemosd1_readings = [
    {
        'DataPoint': 1,
        'LevelValue': u'WaterLevelReadings:',
        'Done': False
    }
]

nodemcu_1_readings = [
    {
        'DataPoint': 1,
        'LightValue': u'LightLevelReadings:',
        'TempValue': u'TempLevelReadings:',
        'HumidValue': u'HumidLevelReadings:',
        'Done': False
    }
]

nodemcu_2_readings = [
	{
        'DataPoint': 1,
        'Value': u'SoilMoistureReadings:',
        'Done': False
    }
]

soilmoisture_readings = [
	{
        'DataPoint': 1,
        'Value': u'SoilMoistureReadings:',
        'Done': False
    }
]

# Add new device here(5)

# The following 3 '@app.route' are examples of how to use GET requests
# from your mPy devices to contact API's and get sensor data from 
# the other nodes. This is where you add functionality.

@app.route('/darksky', methods=['GET'])
def darksky():
    forecast = forecastio.load_forecast(api_key, lat, lng)
    byHour = forecast.hourly()
    data = []
    for hourlyData in byHour.data:
        data.append(hourlyData.precipProbability)
    data = data[:23]
    sumA = 0
    for item in data:
        sumA += float(item)

    if data > 0:
      OverallProb = str(round(sumA / (float(len(data) / 100)), 3))
	  else:
	    OverallProb = 0
    return jsonify({'ChanceOfRainToday % ':OverallProb, 'Data':data}), 201

@app.route('/soilmoisture', methods=['GET'])
# Return the Soil Moisture Level to the Pump Controller when requested
def sendSoilMoisture():
    global smlevel
    return jsonify({'SoilMoistureLevel':smlevel}), 201

@app.route('/tankalert', methods=['GET'])
# Sending a Push notification example
def Push():
    # Replace with your Slack token
    slack_token = "xoxp-66666666666-66666666666-66666666666-83d588fb45145363f4015785a6ebf3f02"
    slack_channel = "#alerts"
    slack_msg = "Tank Level Alert, You must refill before pumping can continue."
    slack = Slacker(slack_token)
    
    # Send Push Notification
    slack.chat.post_message(slack_channel, slack_msg, '<slack user>') 
    return jsonify({'Status':'Push Notification sent'}), 201

# The following 3 '@app.route' are examples of containers which receive JSON 
# sensor data in a POST request, extract data and append to Google Sheet

@app.route('/nodemcu_1', methods=['POST'])
def ENV_reading():
    if not request.json or 'Done' in request.json == True:
        abort(400)
    reading = {
        'DataPoint': nodemcu_1_readings[-1]['DataPoint'] + 1,
        'LightValue': request.json.get('Light', ""),
        'TempValue': request.json.get('Temp', ""),
        'HumidValue': request.json.get('Humid', ""),
        'Time': request.json.get('Time', ""),
        'Done': False
    }
    nodemcu_1_readings.append(reading)
    Lightvariable = str(request.json.get('Light', ""))
    Tempvariable = str(request.json.get('Temp', ""))
    Humidvariable = str(request.json.get('Humid', ""))

    nodemcu_1worksheet.append_row([request.json.get('Time', ""), Lightvariable, Tempvariable, Humidvariable], value_input_option='RAW')
    
    return jsonify({'reading': reading}), 201
    
# NOTE on /nodemcu_2:
# The soil moisture level which the pump contoller can request 
# is saved to a global variable, smlevel in the /nodemcu_2 '@app.route'.
# This is an example of how to pass sensor values between different MCU's

@app.route('/nodemcu_2', methods=['POST'])
def create_reading():
    global smlevel
    if not request.json or 'Done' in request.json == True:
        abort(400)
    reading = {
        'DataPoint': wemosd1_readings[-1]['DataPoint'] + 1,
        'Value': request.json.get('Value', ""),
        'Time': request.json.get('Time', ""),
        'Done': False
    }
    wemosd1_readings.append(reading)
    variable = request.json.get('Value')
    smlevel = variable
    if float(variable) > 100:
        variable = 100.00
    if float(variable) < 0:
        variable = 0.00

    wemosd1worksheet.append_row([request.json.get('Time', ""), str(request.json.get('Value', ""))], value_input_option='RAW')
    
    return jsonify({'reading': reading}), 201

	
@app.route('/wemosd1', methods=['POST'])
def LEVEL_reading():
    if not request.json or 'Done' in request.json == True:
        abort(400)
    reading = {
        'DataPoint': nodemcu_2_readings[-1]['DataPoint'] + 1,
        'Level': request.json.get('Level', ""),        
        'Time': request.json.get('Time', ""),
        'Done': False
    }
    nodemcu_2_readings.append(reading)
    Levelvariable = str(request.json.get('Level', ""))

    nodemcu_2worksheet.append_row([request.json.get('Time', ""), Levelvariable], value_input_option='RAW')
    
    return jsonify({'reading': reading}), 201


# Add new device here(6)

@app.errorhandler(404)
def not_found(error):
    return make_response(jsonify({'error': 'Not found'}), 404)

@app.errorhandler(400)
def not_found(error):
    return make_response(jsonify({'error': 'The request could not be understood by the server due to malformed syntax.'}), 400)

@app.errorhandler(500)
def NotAuth(error):
    gAuth()
    return make_response(jsonify({'error': 'Not Authorised. Reauthorising...'}), 500)


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000, debug=False, threaded=True)

main.py

MicroPython
The main.py file for WemosD1 mini, Pump Controller.
Relay and hcsr04. You need the 5v on Wemos for the hcsr04.
import machine
import urequests
import time
from hcsr04 import HCSR04

try:
	rtc = machine.RTC() # Clock for deepsleep
	rtc.irq(trigger=rtc.ALARM0, wake=machine.DEEPSLEEP)

	relay = machine.Pin(13, machine.Pin.OUT)
	sensor = HCSR04(trigger_pin=15, echo_pin=12)

	H2OLevel = sensor.distance_cm()

	def alert():
		# Send notification to phone through flask server
		resp = urequests.get("http://192.168.1.2:8000/tankalert")
		print(resp.json())
		resp.close()
		
	if H2OLevel > 20:
		alert()
		
	hours = str(time.localtime()[3])
	mins = str(time.localtime()[4])
	secs = str(time.localtime()[5])
	if int(secs) < 10:
		secs = '0' + secs
	if int(mins) < 10:
		mins = '0' + mins
	timestr = hours + ':' + mins + ':' + secs

	url = 'http://192.168.1.2:8000/wemosd1'
	headers = {'content-type': 'application/json'}
	data = '{"Level": "%f", "Time": "%s"}' % (H2OLevel, timestr)
	resp = urequests.post(url, data=data, headers=headers) # Send the request
	print(resp.json())
	
	# If Soil moisture drops below 12% 
	# If its after 20:00 and before 07:00
	# If the distance to the top of the water in tank is less than 20cm
	# Then run the pumps

	r = urequests.get('http://192.168.1.2:8000/soilmoisture')
	SoilMoistVal = float(r.json()['SoilMoistureLevel'])
	# Get the soil moisture level as reported by Nodemcu2 from RPi
	r.close()
	
	if SoilMoistVal < 10:
		# check the weather forecast for rain
		r = urequests.get('http://192.168.1.2:8000/darksky')
		rainToday = r.json()
		Overall = r.json()['ChanceOfRainToday % ']
		
		# if the overall probability of rain reaches 50%
		if float(Overall) > 50:
			rtc.alarm(rtc.ALARM0, 30000)  # Set alarm for 30 seconds
			machine.deepsleep()  # Go to sleep ...

		# if in the next 24 hours, rain is definetly forecast for at least one hour
		elif 1 in rainToday['Data']:
			rtc.alarm(rtc.ALARM0, 30000)  # Set alarm for 30 seconds
			machine.deepsleep()  # Go to sleep ...
			
		if int(hours) >= 20 or int(hours) <= 7:
			if H2OLevel < 20:
				relay.on()
				time.sleep(15)
				relay.off()
				rtc.alarm(rtc.ALARM0, 1000)  # Set alarm for reboot
				machine.deepsleep()  # Go to sleep ...
			else:
				rtc.alarm(rtc.ALARM0, 30000)  # Set alarm for 30 seconds
				machine.deepsleep()  # Go to sleep ...
		else:
			rtc.alarm(rtc.ALARM0, 30000)  # Set alarm for 30 seconds
			machine.deepsleep()  # Go to sleep ...
	else :
		rtc.alarm(rtc.ALARM0, 30000) # Set alarm for 30 seconds
		machine.deepsleep() # Go to sleep ...
	resp.close()
	rtc.alarm(rtc.ALARM0, 30000) # Set alarm for 30 seconds
	machine.deepsleep() # Go to sleep ...

except Exception as e:
	#machine.reset()
	print(e)
	time.sleep(2)
	machine.reset()

main.py

MicroPython
The main.py file for nodemcu_2.
Soil mositure sensor.
import machine
import urequests
import time

rtc = machine.RTC() # Clock for deepsleep
rtc.irq(trigger=rtc.ALARM0, wake=machine.DEEPSLEEP)
adc = machine.ADC(0) # Pin to Read sensor voltage


try:

	######################
	# Sensor calibration #
	######################

	# values on right are inverse * 1000 values on left
	# dry air = 759 (0%) = 1.31752305665349143610013175231
	# water = 382 (100%) = 2.61780104712041884816753926702
	# The Difference     = 1.30027799046692741206740751471
	# 1 %                = 0.0130027799046692741206740751471


	hours = str(time.localtime()[3])
	mins = str(time.localtime()[4])
	secs = str(time.localtime()[5])
	if int(secs) < 10:
		secs = '0' + secs
	if int(mins) < 10:
		mins = '0' + mins
	timestr = hours + ':' + mins + ':' + secs

	SoilMoistVal = (((1 / adc.read())* 1000) / 0.0130027799046692741206740751471) - 101
	if SoilMoistVal > 100:
		SoilMoistVal = 100
	if SoilMoistVal < 0:
		SoilMoistVal = 0

	url = 'http://192.168.1.2:8000/nodemcu_2'
	headers = {'content-type': 'application/json'}
	data = '{"Value": "%s", "Time": "%s"}' % (SoilMoistVal, timestr)
	resp = urequests.post(url, data=data, headers=headers) # Send the request
	print(resp.json())


except:
	machine.reset()

main.py

MicroPython
The main.py file for the nodemcu_2 board.
DHT11 and photoresitor.
import dht
import machine
import urequests
import time

rtc = machine.RTC() # Clock for deepsleep
rtc.irq(trigger=rtc.ALARM0, wake=machine.DEEPSLEEP)

try:
	adc = machine.ADC(0)

	d = dht.DHT11(machine.Pin(4))
	d.measure()
	Temp = d.temperature()
	Humid = d.humidity()
	Light = 100 - (adc.read() / 10.24)
	if Light < 0:
		Light = Light * -1


	hours = str(time.localtime()[3])
	mins = str(time.localtime()[4])
	secs = str(time.localtime()[5])
	if int(secs) < 10:
		secs = '0' + secs
	if int(mins) < 10:
		mins = '0' + mins
	timestr = hours + ':' + mins + ':' + secs

	url = 'http://192.168.1.2:8000/nodemcu_1'
	headers = {'content-type': 'application/json'}
	data = '{"Temp": "%d", "Humid": "%d", "Light": "%f", "Time": "%s"}' % (Temp, Humid, Light, timestr)
	resp = urequests.post(url, data=data, headers=headers) # Send the request
	print(resp.json())
	rtc.alarm(rtc.ALARM0, 30000) # Set alarm for 30 seconds
	machine.deepsleep() # Go to sleep ...

except:
	machine.reset()

boot.py

MicroPython
The WiFi and NTP setup file. Put on all mPy boards.
Change <SSID> and <Password>.
import esp
import network
import machine
import gc
import time
esp.osdebug(None)
from ntptime import settime
gc.collect()

def do_connect():
        sta_if = network.WLAN(network.STA_IF)
        if not sta_if.isconnected():
                print('connecting to network...')
                sta_if.active(True)
                sta_if.connect('<SSID>', '<Password>')
                while not sta_if.isconnected():
                        pass

try:
	do_connect() # Connect to WIFI
	settime()    # Use NTP to set clock
	
except:
	time.sleep(60)
	machine.reset()

Credits

Bobby Leonard

Bobby Leonard

6 projects • 36 followers

Comments