-
Notifications
You must be signed in to change notification settings - Fork 0
Code
This system is mainly written in R. That's the language I'm most comfortable with, so deal with it.
Log on to the Raspberry Pi with ssh. You'll find the current address by looking at Dashboard::Status::I am (Maybe this link works). This number may change depending on the whim of the router/dhcp. Here is a screen shot with the current address circled:

Get onto the local network, then open a terminal window. A typical log-in looks like this:
mnr@Marks-MacBook-Pro ~ % ssh mnr@10.0.0.252
mnr@10.0.0.252's password: The password (last I checked) is "rnm"
You'll need to install R on the Raspberry Pi for any of this to work. Use instructions found at R4pi.org
You'll also need to install the sprinklR package:
# log into the RPi with ssh: ssh mnr@10.0.0.252
# start R: R
devtools::install_github("mnr/sprinklR")
cron is a linux system command that runs specified programs at specified times. This irrigation system all starts with cron.
I've had problems with dhcp lease expirations and have chosen to use the sledgehammer approach of rebooting once per day (at 20:00) and setting cron to run the sprinkler script at 20:30.
Note that I did not set the program to run at reboot (cron language is @reboot). This way, if you reboot the pi several times, it will still only run once per day instead of each time you power down/up.
# This is setup via crontab -e
# m h dom mon dow command
* * * * * Rscript /home/mnr/sprinklR/inst/manualIrrigation.R
30 20 * * * Rscript /home/mnr/sprinklR/inst/startup.R# This is setup via sudo crontab -e
0 20 * * * /sbin/shutdown -r nowcron runs startup.R. The code for all subroutines can be found in the R folder in this repository.
# startup script. Runs at boot
# install.packages("httr2")
# install.packages("devtools")
# library(devtools)
library(httr2)
library(sprinklR)
library(rpigpior)
sinkFile <- file("/home/mnr/sprinklR/sprinklR_log.txt", open = "at")
sink(file = sinkFile,
append = TRUE,
type = "message"
)
sink(file = sinkFile,
append = TRUE,
type = "output"
)
logStatus <- function(theMessage) {
print(paste(date(), theMessage))
}
logStatus("start of run")
yearDay <- as.POSIXlt(Sys.Date())$yday + 1
# create_waterByZone() #create a fresh copy of this matrix
waterByZone <- readRDS("/home/mnr/sprinklR/waterByZone.RDS") # retrieve zone watering matrix
waterByZone <- update_waterbyzone(waterByZone, yearDay) # update with current forecasts
# Calculate needed irrigation ---------------------------------------------
waterByZone <- howMuchToWater(waterByZone,yearDay)
# Trigger irrigation ------------------------------------------------------
# water in front
logStatus("Start of front zone water")
waterFrontSeconds <- conv_mm_to_duration(waterByZone["wateredInFront",yearDay])
irrigate(1, waterFrontSeconds) # turn on front yard
waterByZone["secondsWateredInFront", yearDay] <- waterFrontSeconds
# water in rear
logStatus("Start of rear zone water")
waterRearSeconds <- conv_mm_to_duration(waterByZone["wateredInRear",yearDay])
irrigate(2, waterRearSeconds) # turn on back yard
waterByZone["secondsWateredInRear", yearDay] <- waterRearSeconds
# save all of these calculations
saveRDS(waterByZone, "/home/mnr/sprinklR/waterByZone.RDS")
# Send a heartbeat --------------------------------------------------------
send_heartbeat(waterByZone)
logStatus("end of run")
sink()
This is called by startup.R to calculate how much water is needed, given rainfall and evapotranspiration.
# howMuchToWater()
#' How Much To Water each day
#'
#' compares rainfall from yesterday, today, and tomorrow
#' minus evapotranspiration
#' against today's needed amount of water
#'
#' @param waterByZone matrix showing rainfall, needed and watered
#' @param yearDay day of the year. January 1 = 1
#'
#' @return updated WaterByZone
#' @export
#'
#' @examples
howMuchToWater <- function(waterByZone, yearDay) {
# get the sum rainfall for day before yesterday, yesterday, today, tomorrow, and day after tomorrow
recentRainfall <-
sum(waterByZone["rainfall", (yearDay - 1):(yearDay + 1)])
recentEVOTRP <- sum(waterByZone["evapotranspiration", (yearDay - 1):(yearDay + 1)])
# calculate needed water for front zone
if (waterByZone["neededInFront", yearDay] > 0) {
neededRain <- waterByZone["neededInFront", yearDay] - recentRainfall + recentEVOTRP
waterByZone["wateredInFront", yearDay] <- ifelse(neededRain <= 0, 0, neededRain)
} else {
waterByZone["wateredInFront", yearDay] <- 0
}
# calculate needed water for rear zone
if (waterByZone["neededInRear", yearDay] > 0) {
neededRain <- waterByZone["neededInRear", yearDay] - recentRainfall + recentEVOTRP
waterByZone["wateredInRear", yearDay] <- ifelse(neededRain <= 0, 0, neededRain)
} else {
waterByZone["wateredInRear", yearDay] <- 0
}
return(waterByZone)
}
#' conv_mm_to_duration
#'
#' Converts mm of water to seconds to open valves
#'
#' emitters are spaced every 254 mm and emit 946353 mm^3 per hour
#'
#' @param mmOfWater depth of desired water column
#'
#' @return Seconds to open valve based on above assumptions
#' @export
#'
#' @examples
conv_mm_to_duration <- function(mmOfWater) {
# convert GPH to mm3/second
emitterGPH <- .25 # emitters put out .25 gph
emitterGPsec <- emitterGPH / 60 / 60
mm3_per_gallon <- 3.785e+6
emitter_mm3_sec <- emitterGPsec * mm3_per_gallon
# calculate the volume to be filled with water
emitterDistance_mm <- 254
volumeToFill <- mmOfWater * emitterDistance_mm * emitterDistance_mm
# how long to fill the volume with water?
secondsOpenValve <- volumeToFill / emitter_mm3_sec
return(secondsOpenValve)
}
# update waterbyzone with current forecast
#' update waterByZone with current forecast
#'
#' @param waterByZone a matrix with zone requests and rain forecast
#' @param yearDay day of the year
#'
#' @return updated version of the waterByZone matrix
#' @export
#'
#' @examples
#'
update_waterbyzone <- function(waterByZone, yearDay) {
# get rain forecast -------------------------------------------------------
# https://open-meteo.com/
meteo_response <-
request("https://api.open-meteo.com/v1/forecast") |>
req_url_query(latitude = "45.5234") |>
req_url_query(longitude = "-122.6762") |>
req_url_query(current = "precipitation") |>
req_url_query(daily = "precipitation_sum,precipitation_probability_max,et0_fao_evapotranspiration") |>
req_perform() |>
resp_body_json()
# meteo_response$current$precipitation
# meteo_response$daily$precipitation_sum[[1]]
# update waterByZone matrix --------
# first, current precipitation
# waterByZone["rainfall", yearDay] <- meteo_response$current$precipitation
# next, store forecast
for (index in 1:length(meteo_response$daily$time)) {
yearDayFloat <- as.POSIXlt(meteo_response$daily$time[[index]])$yday
waterByZone["rainfall", yearDayFloat] <-
meteo_response$daily$precipitation_sum[[index]]
waterByZone["evapotranspiration", yearDayFloat] <-
meteo_response$daily$et0_fao_evapotranspiration[[index]]
}
return(waterByZone)
}
# sample return values from meteo
# > meteo_response
# $latitude
# [1] 45.52874
#
# $longitude
# [1] -122.6962
#
# $generationtime_ms
# [1] 0.05698204
#
# $utc_offset_seconds
# [1] 0
#
# $timezone
# [1] "GMT"
#
# $timezone_abbreviation
# [1] "GMT"
#
# $elevation
# [1] 13
#
# $current_units
# $current_units$time
# [1] "iso8601"
#
# $current_units$interval
# [1] "seconds"
#
# $current_units$precipitation
# [1] "mm"
#
#
# $current
# $current$time
# [1] "2024-02-23T05:45"
#
# $current$interval
# [1] 900
#
# $current$precipitation
# [1] 0
#
#
# $daily_units
# $daily_units$time
# [1] "iso8601"
#
# $daily_units$precipitation_sum
# [1] "mm"
#
# $daily_units$precipitation_probability_max
# [1] "%"
#
#
# $daily
# $daily$time
# $daily$time[[1]]
# [1] "2024-02-23"
#
# $daily$time[[2]]
# [1] "2024-02-24"
#
# $daily$time[[3]]
# [1] "2024-02-25"
#
# $daily$time[[4]]
# [1] "2024-02-26"
#
# $daily$time[[5]]
# [1] "2024-02-27"
#
# $daily$time[[6]]
# [1] "2024-02-28"
#
# $daily$time[[7]]
# [1] "2024-02-29"
#
#
# $daily$precipitation_sum
# $daily$precipitation_sum[[1]]
# [1] 0
#
# $daily$precipitation_sum[[2]]
# [1] 0
#
# $daily$precipitation_sum[[3]]
# [1] 0.3
#
# $daily$precipitation_sum[[4]]
# [1] 16.4
#
# $daily$precipitation_sum[[5]]
# [1] 2
#
# $daily$precipitation_sum[[6]]
# [1] 16.9
#
# $daily$precipitation_sum[[7]]
# [1] 15.9
#
#
# $daily$precipitation_probability_max
# $daily$precipitation_probability_max[[1]]
# [1] 0
#
# $daily$precipitation_probability_max[[2]]
# [1] 0
#
# $daily$precipitation_probability_max[[3]]
# [1] 55
#
# $daily$precipitation_probability_max[[4]]
# [1] 100
#
# $daily$precipitation_probability_max[[5]]
# [1] 100
#
# $daily$precipitation_probability_max[[6]]
# [1] 97
#
# $daily$precipitation_probability_max[[7]]
# [1] 94
#
cron runs this script every minute. It handles the request to manually turn on/off zone 1 or zone 2
# Handle a request for manual irrigation
# cron runs every minute
# This program watches for a two way switch
# position one turns on zone 1
# position two turns on zone 2
# position central returns to automatic
library(sprinklR)
library(rpigpior)
zoneToPin <- c(15, 19)
while (rpi_get(zoneToPin[1])) {
irrigate(1,30)
}
while (rpi_get(zoneToPin[2])) {
irrigate(2,30)
}
#' Send_heartbeat
#'
#' Let the web page know we are alive and working
#'
#' @param waterByZone A copy of the waterByZone matrix
#'
#' @return void
#' @export
#'
#' @examples
#'
send_heartbeat <- function(waterByZone) {
# get ip address
theIPaddress <- system("hostname -I", intern = TRUE) |>
strsplit(" ") |>
unlist() |>
(\(x) {x[1]})()
# get uptime
reboot_datetime <- system("uptime -s", intern = TRUE)
# retrieve the log file
sprinklR_logfile <- tail(read.delim("/home/mnr/sprinklR/sprinklR_log.txt", header=FALSE), n = 25)
http_request <- request("https://niemannross.com") |>
req_url_path_append("sprinklR") |>
req_url_path_append("heartbeat.php") |>
req_body_json(list(iam = theIPaddress,
last_reboot = reboot_datetime,
wbz_rainfall = waterByZone["rainfall",],
wbz_NeededZone1 = waterByZone["neededInFront",],
wbz_NeededZone2 = waterByZone["neededInRear",],
wbz_WateredZone1 = waterByZone["wateredInFront",],
wbz_WateredZone2 = waterByZone["wateredInRear",],
wbz_SecondsWateredZone1 = waterByZone["secondsWateredInFront",],
wbz_SecondsWateredZone2 = waterByZone["secondsWateredInRear",],
wbz_evapotranspiration = waterByZone["evapotranspiration",],
logfile = sprinklR_logfile
),
digits = 4)
http_response <- req_perform(http_request)
}
This is located at http://niemannross.com/sprinklR/heartbeat.php and is called by send_heartbeat(waterByZone). heartbeat.php accepts a copy of the current waterByZone data object in json format. It then adds a "modified" day/time to the data, then stores it to heartbeat_data.json, which is used by the dashboard.
<?php
// Takes raw data from the request
$json = file_get_contents('php://input');
// check for valid json input
$json_tmp = json_decode($json, true) ;
if ( isset($json_tmp["wbz_rainfall"]) ) {
// add dateTime to json
$dt = new DateTime("now", new DateTimeZone('America/Los_Angeles'));
$dtf = $dt->format("Y-m-d H:i:s") ;
$json_tmp['modified'] = $dtf ;
//$json_tmp['modified'] = date("Y-m-d H:i:s") ;
$json = json_encode($json_tmp) ;
// save to a local file
$fp = fopen("heartbeat_data.json", "w") ;
fwrite($fp, $json) ;
fclose($fp);
} else {
print("Please don't ping this page");
}
exit();
?>