Garden Irrigation

In [63]:
import boto3
import json
import os
import pandas as pd
import statistics
import math
import datetime
import pytz
from pytz import timezone
from six.moves.urllib import request
from IPython.display import display, HTML
from matplotlib import pyplot as plt

%matplotlib inline
%config InlineBackend.figure_format = 'retina'

Helper to calculate the average time from a list of times

In [64]:
def avg_time(times):
    avg = 0
    for elem in times:
        avg += elem.second + 60*elem.minute + 3600*elem.hour
    avg /= len(times)
    res = str(int(avg/3600)) + ' ' + str(int((avg%3600)/60)) + ' ' + str(int(avg%60))
    return datetime.datetime.strptime(res, "%H %M %S").time()

Retrieve the dataset called 'illumination' from AWS IoT Analytics
this will be used to determine the start and end of the day

In [65]:
iota = boto3.client('iotanalytics')
dataset = "illumination"
dataset_url = iota.get_dataset_content(datasetName = dataset,versionId = "$LATEST")['entries'][0]['dataURI']
df_light = pd.read_csv(dataset_url,low_memory=False)

# Convert the time which is reported UTC to local pacific time and add the index to the dataframe

df_light['datetime']=pd.DatetimeIndex(pd.to_datetime(df_light["received"]/1000, unit='s')) \
    .tz_localize('UTC') \
    .tz_convert('US/Pacific')

df_light.index = df_light['datetime']
In [66]:
response = iota.describe_dataset(datasetName = dataset)
sql = response["dataset"]["actions"][0]["queryAction"]["sqlQuery"]

Get the minimum and maximum light levels daily, ignoring the first and last days which have incomplete data

In [67]:
g_min = df_light.resample('D')['illumination'].min()[1:-1]
g_max = df_light.resample('D')['illumination'].max()[1:-1]

start = g_min.index.min()
end = g_min.index.max()

range =g_max.mean() - g_min.mean()
percent=25/100

morning = math.ceil(g_min.mean()+range*percent)
evening = math.ceil(g_max.mean()-range*percent)

morning,evening,start,end
Out[67]:
(34,
 73,
 Timestamp('2018-08-24 00:00:00-0700', tz='US/Pacific', freq='D'),
 Timestamp('2018-08-29 00:00:00-0700', tz='US/Pacific', freq='D'))
In [68]:
display(HTML('<h1 style="font-size:32px">'+sql+"</h1>"))

SELECT * FROM telescope_data where __dt >= current_date - interval '7' day AND unit='lumen' order by epoch desc

In [69]:
df_light = df_light[((df_light.datetime >= start) & (df_light.datetime <= end))]
df_light.plot(title='Light Levels',kind='area',x='datetime',y='illumination',figsize=(20,8),color='orange',linewidth=2,grid=True)
Out[69]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f1cc272bb00>

Find the average time when the morning mean light level is reached

In [70]:
mornings=[]
df_light.sort_values(by='datetime', ascending=True, inplace=True)
for day in g_min.index:
    thisDay = day.date()
    for index, row in df_light.iterrows():
        if (thisDay == row['datetime'].date()):
            if (row['illumination'] > morning):
                if (index.hour < 12) :
                    mornings.append(index)
                    print (index,row['illumination'])
                    break

water_on = avg_time(mornings)
display(HTML('<h1 style="font-size:48px;margin-top:4px">Water ON hour ('+str("%.0f" % water_on.hour)+'), minute ('+str("%.0f" % water_on.minute)+')</h1>'))
2018-08-24 06:37:41.793000-07:00 34.082029999999996
2018-08-25 06:18:40.920000-07:00 34.96094
2018-08-26 06:27:52.239000-07:00 34.17969
2018-08-27 06:24:08.232000-07:00 34.375
2018-08-28 06:18:39.213000-07:00 34.082029999999996

Water ON hour (6), minute (25)

Now plot the last 3 days of temperature readings - the hotter it has been, the more water we will deliver

In [71]:
dataset = "temperature"
dataset_url = iota.get_dataset_content(datasetName = dataset,versionId = "$LATEST")['entries'][0]['dataURI']
df_temp = pd.read_csv(dataset_url)

# Convert the time which is reported UTC to local pacific time and add the index to the dataframe

df_temp['datetime']=pd.DatetimeIndex(pd.to_datetime(df_temp["received"]/1000, unit='s')) \
    .tz_localize('UTC') \
    .tz_convert('US/Pacific')

df_temp.index = df_temp['datetime']
In [72]:
response = iota.describe_dataset(datasetName = dataset)
sql = response["dataset"]["actions"][0]["queryAction"]["sqlQuery"]
display(HTML('<h1 style="font-size:32px">'+sql+"</h1>"))

SELECT * FROM telescope_data where __dt >= current_date - interval '7' day AND (unit='Celcius' OR unit='C') order by epoch desc

In [73]:
df_temp = df_temp[\
                  ((df_temp.datetime >= (datetime.datetime.now() - datetime.timedelta(days=3))) \
                   & (df_temp['full_topic'].str.contains('barometer'))) \
                 ]

df_temp.plot(title='Temperature',kind='line',x='datetime',y='temperature',figsize=(20,8),color='red',linewidth=8,grid=True)
Out[73]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f1cbc102f28>

Formula to calculate how long to water for is based on last 72 hours temperature

In [36]:
conditions = df_temp["temperature"].quantile([0.1,0.5,0.9])
duration = math.ceil(5*max(0,conditions[0.1]-10) + 4*max(0,conditions[0.5]-25) + 5*max(0,conditions[0.9]-29))
In [37]:
winter = datetime.datetime.now().month in [11,12,1,2]
if (winter) : 
    duration =0
display(HTML('<h1 style="font-size:48px">Water duration = '+str("%.0f" % duration)+' minutes</h1>'))

Water duration = 85 minutes

Prepare watering schedules for the next day

In [38]:
water_off = datetime.datetime.combine(datetime.datetime.now(), water_on) + datetime.timedelta(minutes=duration)
water_on = datetime.datetime.combine(datetime.datetime.now(), water_on)

print('water on (local)=',water_on)
print('water off (local)=',water_off)

water_on_local = water_on
water_off_local = water_off

tz = pytz.timezone('US/Pacific')

water_on = tz.localize(water_on.replace(tzinfo=None)).astimezone(timezone('UTC'))
water_off = tz.localize(water_off.replace(tzinfo=None)).astimezone(timezone('UTC'))

print('water on (utc)=',water_on)
print('water off (utc)=',water_off)
water on (local)= 2018-08-23 06:06:10
water off (local)= 2018-08-23 07:31:10
water on (utc)= 2018-08-23 13:06:10+00:00
water off (utc)= 2018-08-23 14:31:10+00:00

Is it going to rain in the next 24 hours, invoke a lambda to find out

In [39]:
lambdaClient = boto3.client('lambda')
response = lambdaClient.invoke(FunctionName='IsItGoingToRain')
result = json.loads(response['Payload'].read().decode("utf-8"))
willItRain = result

Configure the cloudwatch events that control the water

In [40]:
ruleStatus = 'DISABLED' if (willItRain) else 'ENABLED'
cwe = boto3.client('events')

response = cwe.put_rule(
    Name='water-on',\
    ScheduleExpression='cron('+str(water_on.minute)+' '+str(water_on.hour)+' ? * * *)',\
    State=ruleStatus,\
    Description='Autogenerated rule to turn the water ON at the specified time')

response = cwe.put_rule(
    Name='water-off',\
    ScheduleExpression='cron('+str(water_off.minute)+' '+str(water_off.hour)+' ? * * *)',\
    State=ruleStatus,\
    Description='Autogenerated rule to turn the water OFF at the specified time')

Report our configuration so we can diagnose issues later if needed

In [41]:
output = {"generated":datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),\
          "duration":duration,\
          "on":water_on.strftime("%H:%M:%S"),\
          "off":water_off.strftime("%H:%M:%S"),\
          "winter":winter,\
          "rain_forecast":willItRain,\
          "p10":round(conditions[0.1],1),\
          "p50":round(conditions[0.5],1),\
          "p90":round(conditions[0.9],1)\
         }
json.dumps(output)
Out[41]:
'{"generated": "2018-08-23 04:45:01", "duration": 85, "on": "13:06:10", "off": "14:31:10", "winter": false, "rain_forecast": false, "p10": 21.4, "p50": 27.0, "p90": 32.9}'
In [17]:
iot = boto3.client('iot-data', region_name='us-west-2')
response = iot.publish(topic='rtjm/irrigation',qos=1,payload=json.dumps(output))
In [18]:
message = 'Next water on at '+water_on_local.strftime("%H:%M:%S")+' for '+str(duration)+' minutes. Rain forecast is '+str(willItRain)+'. Recent highs of '+str(round(conditions[0.9],1))+' celcius'
sns = boto3.client('sns')
response = sns.publish(TopicArn='arn:aws:sns:us-west-2:165642864119:information-service',Message=message)

Plot how much water has been delivered in the last day

In [19]:
dataset = "telescope_daily"
dataset_url = iota.get_dataset_content(datasetName = dataset,versionId = "$LATEST")['entries'][0]['dataURI']
df = pd.read_csv(dataset_url,low_memory=False)
df['datetime'] = pd.to_datetime(df["received"]/1000, unit='s')
df.index = df['datetime']
analysis = df[((df['flow'] >= 0))]
In [20]:
response = iota.describe_dataset(datasetName = dataset)
sql = response["dataset"]["actions"][0]["queryAction"]["sqlQuery"]
display(HTML('<h1 style="font-size:32px">'+sql+"</h1>"))

select * from telescope_data where __dt >= current_date - interval '1' day

In [21]:
analysis.plot(title='Irrigation Flow Rates',kind='area',x='datetime',y='flow',figsize=(20,8),color='blue',linewidth=2,grid=True)
Out[21]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f1cb8cce9e8>