Skip to content
Mark Niemann-Ross edited this page Apr 16, 2025 · 19 revisions

How this works: the code

This system is mainly written in R. That's the language I'm most comfortable with, so deal with it.

Log on

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"

R

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

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 now

startup.R

cron 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()

howmuchtowater.R

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.R


#' 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.R

# 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
#

manualIrrigation.R

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.R

#' 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)
}

heartbeat.php

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





Clone this wiki locally