package org.bensteele.jirrigate;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.DecimalFormat;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.naming.ConfigurationException;
import joptsimple.OptionException;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;
import org.apache.log4j.RollingFileAppender;
import org.bensteele.jirrigate.controller.Controller;
import org.bensteele.jirrigate.controller.Controller.ControllerType;
import org.bensteele.jirrigate.controller.EtherRain8Controller;
import org.bensteele.jirrigate.controller.zone.EtherRain8Zone;
import org.bensteele.jirrigate.controller.zone.Zone;
import org.bensteele.jirrigate.weather.WeatherStation;
import org.bensteele.jirrigate.weather.WeatherStation.WeatherStationType;
import org.bensteele.jirrigate.weather.weatherunderground.WeatherUndergroundStation;
import org.joda.time.DateTime;
import org.joda.time.DateTimeConstants;
import org.joda.time.LocalTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
/**
* The main class for the jirrigate application.
* <p>
* Controls one or more {@link Controller} and (optionally) one or more {@link WeatherStation}.
*
* @author Ben Steele (ben@bensteele.org)
*/
public class Irrigator {
private static final String VERSION = "1.1";
private static final String LICENSE_HEADER = "Jirrigate v" + VERSION
+ " Copyright (C) 2014 Ben Steele\n" + "This program comes with ABSOLUTELY NO WARRANTY;\n"
+ "This is free software, and you are welcome to redistribute it\n"
+ "under certain conditions; type 'show license' for details.\n";
private static final Logger LOG = Logger.getRootLogger();
private static String configFilePath;
public static void main(String[] args) throws IOException, ConfigurationException {
System.out.println(LICENSE_HEADER);
// TODO fix telnet implementation
// final Thread t = new Thread(new TelnetServer(32666));
// t.start();
final Properties config = parseConfigurationFile(args);
final Irrigator i = new Irrigator(config);
try {
System.out.println("Processing configuration file...");
i.processConfiguration();
System.out.println("Done!");
} catch (final ConfigurationException e) {
System.out.println(e.getMessage());
System.exit(1);
}
final Console c = new Console(i);
c.startConsole();
}
protected static Properties parseConfigurationFile(String[] args) throws IOException {
final OptionParser parser = new OptionParser() {
{
accepts("config").withRequiredArg().required().ofType(String.class)
.describedAs("Path to the configuration file.");
}
};
OptionSet options = null;
try {
options = parser.parse(args);
} catch (final OptionException e) {
parser.printHelpOn(System.out);
System.exit(1);
}
configFilePath = (String) options.valueOf("config");
final Properties config = new Properties();
config.load(new FileInputStream(configFilePath));
return config;
}
protected void reloadConfigurationFromFile() throws FileNotFoundException, IOException {
config.load(new FileInputStream(configFilePath));
}
private final ExecutorService requestExecutor = Executors.newSingleThreadExecutor();
private final Set<Integer> wateringDays = new TreeSet<Integer>();
private final Set<Controller> controllers = new HashSet<Controller>();
private final Set<WeatherStation> weatherStations = new HashSet<WeatherStation>();
private final Properties config;
private final DecimalFormat twoDecimals = new DecimalFormat("###.##");
private final PatternLayout logPattern = new PatternLayout("%d %-5p- %m%n");
private LocalTime wateringStartTime;
private int rainDaysToLookBack;
private int rainDaysToLookAhead;
private int popThreshold;
private double totalRainAmountThresholdInMilliMetres = Double.MAX_VALUE;
private double currentRainAmountThresholdInMilliMetres = Double.MAX_VALUE;
private double currentWindThresholdInKmph = Double.MAX_VALUE;
private double maxTempThresholdInCelcius = Double.MAX_VALUE;
private double minTempThresholdInCelcius = Double.MAX_VALUE;
private String logPath;
private double weatherMultiplierValue = 1.0; // Default value of no multiply.
private double weatherMultiplierMaxTemp = 1000.00; // Default to value that won't false positive.
private int weatherMultiplierDaysToLookAhead;
/**
* Creates a single instance of an Irrigator and all of its child
* {@link Controller} and {@link WeatherStation} Objects. The configuration of
* these Objects is derived from the configuration properties passed into the
* constructor and may have the following values:
*
* <pre>
* X means a numeric value, generally starting at 1 and incrementing as required
* (i.e. controller3 would indicate your third controller configuration).
*
* logging_directory - The path to the directory to write the jirrigate.log file.
*
* controllerX_name - Friendly name. <required>
* controllerX_ip - IP address. <required>
* controllerX_port - Port to connect to. <required>
* controllerX_username - Login Username. <optional>
* controllerX_password - Login Password. <optional>
* controllerX_type - Type of this controller, i.e. EtherRain8. <required>
*
* controllerX_zoneX_name - Friendly name. <required>
* controllerX_zoneX_id - ID of this zone on the controller. <required>
* controllerX_zoneX_duration - How long to irrigate this zone, accepts s/m/h delimiters. <required>
*
* watering_days - Days to irrigate, i.e. monday,wednesday,saturday. <required>
* watering_start_time - Time to start irrigation in 24-hour format i.e. 23:30 for 11:30pm. <required>
*
* weather_watcher - Will stop an active irrigation if thresholds are exceeded, default true. Accepts true/false.<optional>
*
* <WeatherStation as a whole is optional>
* weatherstationX_name - Friendly name.<required>
* weatherstationX_type - Type i.e. wunderground.<required>
* weatherstationX_staionid - Station specific ID i.e. AUSOUTH123.<required>
* weatherstationX_api - API key.<required>
*
* <Thresholds are optional and require a valid WeatherStation to be accepted>
* weather_threshold_rain_days_to_look_ahead - # of days in advance to use for rain thresholds.<optional>
* weather_threshold_rain_days_to_look_back - # of days in past to use for rain thresholds.<optional>
* weather_threshold_total_rain_amount - Total rain from weather_threshold_rain_days_to_look_ahead, accepts mm/in.<optional>
* weather_threshold_current_rain_amount - Amount of rain from today, accepts mm/in.<optional>
* weather_threshold_pop - Chance of rain over the weather_threshold_rain_days_to_look_ahead period.<optional>
* weather_threshold_wind_speed - Current wind speed, accepts kph/mph.<optional>
* weather_threshold_min_temp - Current temperature low cut off, accepts C/F.<optional>
* weather_threshold_max_temp - Current temperature high cut off, accepts C/F.<optional>
*
* <Weather multiplier is optional and require a valid WeatherStation to be accepted>
* weather_multiplier_max_temp - Temperature to trigger the multiplier, accepts C/F.<optional>
* weather_multiplier_value - Amount to multiply the normal irrigation time for i.e. 1.5 for 50% extra.<optional>
* weather_multiplier_days_to_look_ahead - The amount of forecast days ahead to use for gathering data, accepts a whole number.<optional>
*
* @param config
* The configuration to be loaded into the {@link Irrigator}.
* @throws IOException
* If log4j has trouble creating the log file.
* @throws ConfigurationException
* If the configuration contains invalid or missing properties.
*/
public Irrigator(Properties config) throws IOException, ConfigurationException {
this.config = config;
// Logging setup
if (config.getProperty("logging_directory") == null) {
logPath = System.getProperty("user.dir");
} else {
final File file = new File(config.getProperty("logging_directory"));
if (!file.isDirectory()) {
throw new ConfigurationException("ERROR: Must specify a logging_directory");
} else {
logPath = config.getProperty("logging_directory");
}
}
final RollingFileAppender fileAppender = new RollingFileAppender(logPattern, logPath + File.separator
+ "jirrigate.log", true);
fileAppender.setMaxFileSize("10MB");
fileAppender.setMaxBackupIndex(10);
LOG.addAppender(fileAppender);
LOG.setLevel(Level.INFO);
// Main irrigator thread.
requestExecutor.execute(new Runnable() {
@Override
public void run() {
startIrrigator();
startWeatherWatcher();
}
});
}
public Properties getConfig() {
return config;
}
public Controller getController(String controllerName) {
for (final Controller c : controllers) {
if (c.getName().matches(controllerName)) {
return c;
}
}
return null;
}
public Set<Controller> getControllers() {
return this.controllers;
}
public double getCurrentRainAmountThresholdInMilliMetres() {
return Double.valueOf(twoDecimals.format(currentRainAmountThresholdInMilliMetres));
}
public double getCurrentWindThresholdInKmph() {
return Double.valueOf(twoDecimals.format(currentWindThresholdInKmph));
}
public double getMaxTempThresholdInCelcius() {
return Double.valueOf(twoDecimals.format(maxTempThresholdInCelcius));
}
public double getMinTempThresholdInCelcius() {
return Double.valueOf(twoDecimals.format(minTempThresholdInCelcius));
}
public int getPopThreshold() {
return this.popThreshold;
}
public int getRainDaysToLookAhead() {
return this.rainDaysToLookAhead;
}
public int getRainDaysToLookBack() {
return rainDaysToLookBack;
}
public double getTotalRainAmountThresholdInMilliMetres() {
return Double.valueOf(twoDecimals.format(totalRainAmountThresholdInMilliMetres));
}
public String getVersion() {
return VERSION;
}
public Set<Integer> getWateringDays() {
return this.wateringDays;
}
public LocalTime getWateringStartTime() {
return this.wateringStartTime;
}
public WeatherStation getWeatherStation(String weatherStationName) {
for (final WeatherStation w : weatherStations) {
if (w.getName().matches(weatherStationName)) {
return w;
}
}
return null;
}
public Set<WeatherStation> getWeatherStations() {
return this.weatherStations;
}
protected boolean isIrrigating() {
for (final Controller c : controllers) {
if (c.isIrrigating()) {
return true;
}
}
return false;
}
/**
* Returns the time and date the next irrigation is due based on the watering_days and
* watering_start_time. It does not take into account whether or not any of the {@link Controller}
* are active.
*
* @return The time and date of the next irrigation for any controller under this irrigator's
* control.
*/
protected DateTime nextIrrigationAt() {
DateTime dt = new DateTime();
for (int i = 0; i < 7; i++) {
for (final int dayOfWeek : wateringDays) {
if (dayOfWeek == (dt.getDayOfWeek())) {
// If it's the first run then we may match the same day we are currently on, in this case
// we need to check that we don't report a time in the past. Validate that the hour and
// minute right now are not past the scheduled watering time. If it's not the first run
// then it's ok to let through.
if (i != 0 || (i == 0 && dt.toLocalTime().isBefore(wateringStartTime))) {
// Reset the hour to 0 and increment until we match the watering hour.
dt = dt.withHourOfDay(0);
while (dt.getHourOfDay() < wateringStartTime.getHourOfDay()) {
dt = dt.plusHours(1);
}
// Reset the minute to 0 and increment until we match the watering minute.
dt = dt.withMinuteOfHour(0);
while (dt.getMinuteOfHour() < wateringStartTime.getMinuteOfHour()) {
dt = dt.plusMinutes(1);
}
return dt;
}
}
}
dt = dt.plusDays(1);
}
return null;
}
public void processConfiguration() throws ConfigurationException, IOException {
// Process the weather stations.
processWeatherStationConfiguration();
// Process the weather thresholds.
processWeatherThresholdConfiguration();
// Process the days that irrigation can be performed.
processWateringDaysConfiguration();
// Process the days that irrigation can be performed.
processWateringStartTimeConfiguration();
// Process the extreme weather multiplier factor for irrigation duration.
processWeatherMultiplierConfiguration();
// Process the irrigation controllers.
processControllerConfiguration();
}
public void processControllerConfiguration() throws ConfigurationException, IOException {
controllers.clear();
for (int i = 1; i < Integer.MAX_VALUE; i++) {
final String controllerValue = "controller" + i;
final String controllerName = config.getProperty(controllerValue + "_name");
if (controllerName == null) {
// No more controllers to parse.
break;
} else {
// IP Address
if (config.getProperty(controllerValue + "_ip") == null) {
throw new ConfigurationException("ERROR: Unable to parse IP address of "
+ controllerValue + "_ip");
}
InetAddress ipAddress;
try {
ipAddress = InetAddress.getByName(config.getProperty(controllerValue + "_ip"));
} catch (final UnknownHostException e) {
throw new ConfigurationException("ERROR: Unable to parse IP address of "
+ controllerValue + "_ip");
}
// Port
if (config.getProperty(controllerValue + "_port") == null) {
throw new ConfigurationException("ERROR: Unable to parse port of " + controllerValue
+ "_port");
}
int port;
try {
port = Integer.parseInt(config.getProperty(controllerValue + "_port"));
} catch (final NumberFormatException e) {
throw new ConfigurationException("ERROR: Unable to parse port of " + controllerValue
+ "_port");
}
// Username
final String username = config.getProperty(controllerValue + "_username");
// Password
final String password = config.getProperty(controllerValue + "_password");
// Instantiate controller based on controller_type value.
if (config.getProperty(controllerValue + "_type") == null) {
throw new ConfigurationException("ERROR: " + "no controller type for " + controllerValue
+ "_type");
}
Controller c = null;
final String controllerType = config.getProperty(controllerValue + "_type");
for (final ControllerType type : Controller.ControllerType.values()) {
if (controllerType.toUpperCase().matches(type.name())) {
if (type.name().matches("ETHERRAIN8")) {
c = new EtherRain8Controller(controllerName, ipAddress, port, username, password, LOG);
}
}
}
if (c == null) {
throw new ConfigurationException("ERROR: " + controllerType
+ " is not a valid controller type for " + controllerValue + "_type");
}
// Process the zones for this controller.
processControllerZonesConfiguration(c, controllerValue);
// Controller configuration has passed, add it to the list of controllers.
controllers.add(c);
}
}
}
/**
* Processes the {@link Zone} configurations for a given {@link Controller}. It will convert any
* duration values into seconds but will leave the specifics on what is and isn't valid for a
* duration to the {@link Zone} implementation.
*
* @param c The {@link Controller} the {@link Zone} belong to.
* @param controllerValue The order in which it's processed from the configuration ie
* "controller3" for the third controller.
* @throws ConfigurationException If the configuration is invalid.
*/
private void processControllerZonesConfiguration(Controller c, String controllerValue)
throws ConfigurationException {
for (int j = 1; j < Integer.MAX_VALUE; j++) {
final String zone = controllerValue + "_zone" + j;
final String zoneName = config.getProperty(zone + "_name");
if (zoneName == null) {
// No more zones to parse.
break;
} else {
final String zoneId = config.getProperty(zone + "_id");
if (zoneId == null) {
throw new ConfigurationException("ERROR: Could not find configuration for " + zone
+ "_id");
}
// Create the zone, it will automatically add itself to the controller.
Zone z = null;
try {
if (c.getClass().equals(EtherRain8Controller.class)) {
z = new EtherRain8Zone(c, zoneName, 0, zoneId);
}
} catch (final IllegalArgumentException e) {
throw new ConfigurationException(e.getMessage());
}
// Duration is an optional value expressed in [value][s|m|h] where s=seconds, m=minutes
// and h=hours in the configuration file. This will be converted into seconds for use
// with the controllers.
final String zoneDuration = config.getProperty(zone + "_duration");
if (zoneDuration != null) {
try {
final long durationValue = Long
.parseLong(zoneDuration.substring(0, zoneDuration.length() - 1));
final String durationDelimiter = zoneDuration.substring(zoneDuration.length() - 1);
if (durationDelimiter.equalsIgnoreCase("s")) {
z.setDuration(durationValue);
} else if (durationDelimiter.equalsIgnoreCase("m")) {
z.setDuration(durationValue * 60);
} else if (durationDelimiter.equalsIgnoreCase("h")) {
z.setDuration((durationValue * 60) * 60);
} else {
throw new ConfigurationException("ERROR: Unable to parse duration of " + zone
+ "_duration");
}
} catch (final NumberFormatException e) {
throw new ConfigurationException("ERROR: Unable to parse duration of " + zone
+ "_duration");
} catch (final IllegalArgumentException e) {
throw new ConfigurationException(e.getMessage());
}
}
}
}
}
/**
* Parse the configuration file for names of days of the week from the "watering_days" value.
* Convert to java.util.Calendar.DAY_OF_WEEK int value and store for scheduling use.
*
* @param config The configuration file.
* @throws ConfigurationException If the configuration is invalid.
*/
public void processWateringDaysConfiguration() throws ConfigurationException {
final String errorMsg = "ERROR: Could not read watering_days from configuration file";
if (config.getProperty("watering_days") == null) {
throw new ConfigurationException(errorMsg);
}
final String[] wateringDaysArg = config.getProperty("watering_days").split(",");
for (String s : wateringDaysArg) {
s = s.trim();
if (s.equalsIgnoreCase("sunday")) {
wateringDays.add(DateTimeConstants.SUNDAY);
}
if (s.equalsIgnoreCase("monday")) {
wateringDays.add(DateTimeConstants.MONDAY);
}
if (s.equalsIgnoreCase("tuesday")) {
wateringDays.add(DateTimeConstants.TUESDAY);
}
if (s.equalsIgnoreCase("wednesday")) {
wateringDays.add(DateTimeConstants.WEDNESDAY);
}
if (s.equalsIgnoreCase("thursday")) {
wateringDays.add(DateTimeConstants.THURSDAY);
}
if (s.equalsIgnoreCase("friday")) {
wateringDays.add(DateTimeConstants.FRIDAY);
}
if (s.equalsIgnoreCase("saturday")) {
wateringDays.add(DateTimeConstants.SATURDAY);
}
}
if (wateringDays.isEmpty()) {
throw new ConfigurationException(errorMsg);
}
}
/**
* Parse the configuration file for names of days of the week from the "watering_start_time"
* value. Expects a 24 hour ":" separated value, stores it as Joda LocalTime.
*
* @param config The configuration file.
* @throws ConfigurationException If the configuration is invalid.
*/
public void processWateringStartTimeConfiguration() throws ConfigurationException {
if (config.getProperty("watering_start_time") == null) {
throw new ConfigurationException(
"ERROR: Could not read watering_start_time from configuration file");
}
final String startTimeString = config.getProperty("watering_start_time");
// Expect 24 hour time format ie 23:10 for 11:10PM.
final String[] timeSplit = startTimeString.split(":");
try {
final int hour = Integer.parseInt(timeSplit[0]);
final int minute = Integer.parseInt(timeSplit[1]);
final LocalTime wateringStartTime = new LocalTime(hour, minute);
this.wateringStartTime = wateringStartTime;
} catch (final NumberFormatException e) {
throw new ConfigurationException("ERROR: Unable to parse watering_start_time value");
}
}
public void processWeatherStationConfiguration() throws ConfigurationException {
weatherStations.clear();
for (int i = 1; i < Integer.MAX_VALUE; i++) {
final String weatherStationValue = "weatherstation" + i;
final String weatherStationName = config.getProperty(weatherStationValue + "_name");
if (weatherStationName == null) {
// No more weather stations to parse.
break;
} else {
// Instantiate weather station based on weatherstation_type value.
if (config.getProperty(weatherStationValue + "_type") == null) {
throw new ConfigurationException("ERROR: " + "no weather station type for "
+ weatherStationValue + "_type");
}
WeatherStation w = null;
final String weatherStationType = config.getProperty(weatherStationValue + "_type");
for (final WeatherStationType type : WeatherStation.WeatherStationType.values()) {
if (weatherStationType.toUpperCase().matches(type.name())) {
if (type.name().matches("WUNDERGROUND")) {
if (config.getProperty(weatherStationValue + "_stationid") == null) {
throw new ConfigurationException("ERROR: " + "no stationid type for "
+ weatherStationValue + "_stationid");
}
final String stationId = config.getProperty(weatherStationValue + "_stationid");
if (config.getProperty(weatherStationValue + "_api") == null) {
throw new ConfigurationException("ERROR: " + "no api key for "
+ weatherStationValue + "_api");
}
final String api = config.getProperty(weatherStationValue + "_api");
w = new WeatherUndergroundStation(weatherStationName, api, stationId);
}
}
}
if (w == null) {
throw new ConfigurationException("ERROR: " + weatherStationType
+ " is not a valid weather station type for " + weatherStationValue + "_type");
}
// Weather Station configuration has passed, add it to the list of weatherstations.
weatherStations.add(w);
}
}
}
public void processWeatherThresholdConfiguration() throws ConfigurationException {
final String NO_WEATHER_STATION = "ERROR: cannot have weather_threshold values without a weatherstation configured";
if (config.getProperty("weather_threshold_rain_days_to_look_back") != null) {
if (weatherStations.isEmpty()) {
throw new ConfigurationException(NO_WEATHER_STATION);
}
if (config.getProperty("weather_threshold_total_rain_amount") == null) {
throw new ConfigurationException(
"ERROR: must supply a weather_threshold_total_rain_amount value");
}
rainDaysToLookBack = Math.abs(Integer.parseInt(config
.getProperty("weather_threshold_rain_days_to_look_back")));
final String totalRain = config.getProperty("weather_threshold_total_rain_amount");
final String delimiter = totalRain.substring(totalRain.length() - 2);
totalRainAmountThresholdInMilliMetres = Math.abs(Double.parseDouble(totalRain.substring(0,
totalRain.length() - 2)));
if (delimiter.equalsIgnoreCase("in")) {
totalRainAmountThresholdInMilliMetres = (totalRainAmountThresholdInMilliMetres * 25.4);
} else if (!delimiter.equalsIgnoreCase("mm")) {
throw new ConfigurationException(
"ERROR: must supply a delimiter of \"mm\" or \"in\" to weather_threshold_total_rain_amount value");
}
}
if (config.getProperty("weather_threshold_current_rain_amount") != null) {
if (weatherStations.isEmpty()) {
throw new ConfigurationException(NO_WEATHER_STATION);
}
final String currentRain = config.getProperty("weather_threshold_current_rain_amount");
final String delimiter = currentRain.substring(currentRain.length() - 2);
currentRainAmountThresholdInMilliMetres = Math.abs(Double.parseDouble(currentRain.substring(
0, currentRain.length() - 2)));
if (delimiter.equalsIgnoreCase("in")) {
currentRainAmountThresholdInMilliMetres = (currentRainAmountThresholdInMilliMetres * 25.4);
} else if (!delimiter.equalsIgnoreCase("mm")) {
throw new ConfigurationException(
"ERROR: must supply a delimiter of \"mm\" or \"in\" to weather_threshold_current_rain_amount");
}
}
if (config.getProperty("weather_threshold_wind_speed") != null) {
if (weatherStations.isEmpty()) {
throw new ConfigurationException(NO_WEATHER_STATION);
}
final String currentWind = config.getProperty("weather_threshold_wind_speed");
final String delimiter = currentWind.substring(currentWind.length() - 3);
currentWindThresholdInKmph = Math.abs(Double.parseDouble(currentWind.substring(0,
currentWind.length() - 3)));
if (delimiter.equalsIgnoreCase("mph")) {
currentWindThresholdInKmph = (currentWindThresholdInKmph * 1.6);
} else if (!delimiter.equalsIgnoreCase("kph")) {
throw new ConfigurationException(
"ERROR: must supply a delimiter of \"kph\" or \"mph\" to weather_threshold_wind_speed");
}
}
if (config.getProperty("weather_threshold_max_temp") != null) {
if (weatherStations.isEmpty()) {
throw new ConfigurationException(NO_WEATHER_STATION);
}
final String maxTemp = config.getProperty("weather_threshold_max_temp");
final String delimiter = maxTemp.substring(maxTemp.length() - 1);
maxTempThresholdInCelcius = Double.parseDouble(maxTemp.substring(0, maxTemp.length() - 1));
if (delimiter.equalsIgnoreCase("F")) {
maxTempThresholdInCelcius = (maxTempThresholdInCelcius - 32) * (5.0 / 9.0);
} else if (!delimiter.equalsIgnoreCase("C")) {
throw new ConfigurationException(
"ERROR: must supply a delimiter of \"C\" or \"F\" to weather_threshold_max_temp");
}
}
if (config.getProperty("weather_threshold_min_temp") != null) {
if (weatherStations.isEmpty()) {
throw new ConfigurationException(NO_WEATHER_STATION);
}
final String minTemp = config.getProperty("weather_threshold_min_temp");
final String delimiter = minTemp.substring(minTemp.length() - 1);
minTempThresholdInCelcius = Double.parseDouble(minTemp.substring(0, minTemp.length() - 1));
if (delimiter.equalsIgnoreCase("F")) {
minTempThresholdInCelcius = (minTempThresholdInCelcius - 32) * (5.0 / 9.0);
} else if (!delimiter.equalsIgnoreCase("C")) {
throw new ConfigurationException(
"ERROR: must supply a delimiter of \"C\" or \"F\" to weather_threshold_min_temp");
}
}
if (config.getProperty("weather_threshold_rain_days_to_look_ahead") != null) {
if (weatherStations.isEmpty()) {
throw new ConfigurationException(NO_WEATHER_STATION);
}
if (config.getProperty("weather_threshold_pop") == null) {
throw new ConfigurationException("ERROR: must supply a weather_threshold_pop value");
}
rainDaysToLookAhead = Math.abs(Integer.parseInt(config
.getProperty("weather_threshold_rain_days_to_look_ahead")));
popThreshold = Math.abs(Integer.parseInt(config.getProperty("weather_threshold_pop")));
}
}
public void startIrrigator() {
final DateTimeFormatter formatter = DateTimeFormat.forPattern("dd/MM/yyyy HH:mm");
LOG.info("Irrigator v" + VERSION + " started");
LOG.info("Next irrigation due at " + nextIrrigationAt().toString(formatter));
while (true) {
if (!isIrrigating() && timeToIrrigate()) {
for (final Controller c : controllers) {
if (c.isActive()) {
c.irrigationRequest(c.generateDefaultIrrigationRequest(getWeatherMultiplier()));
}
}
LOG.info("Next irrigation due at " + nextIrrigationAt().toString(formatter));
}
try {
// Try to irrigate once a minute.
Thread.sleep(60000);
} catch (final InterruptedException e) {
System.out.println("Thread interrupted: " + e.getMessage());
System.exit(1);
}
}
}
protected boolean thresholdsExceeded() {
for (final WeatherStation ws : weatherStations) {
if (currentRainAmountThresholdInMilliMetres <= ws.getTodaysRainfallMilliLitres()) {
LOG.info("Threshold exceeded: today's rainfall " + ws.getTodaysRainfallMilliLitres()
+ "mm greater than threshold of " + currentRainAmountThresholdInMilliMetres + "mm");
return true;
}
if (currentWindThresholdInKmph <= ws.getCurrentWindspeedKiloMetresPerHour()) {
LOG.info("Threshold exceeded: current windspeed "
+ ws.getCurrentWindspeedKiloMetresPerHour() + "kph greater than threshold of "
+ currentWindThresholdInKmph + "kph");
return true;
}
if (maxTempThresholdInCelcius <= ws.getCurrentTemperatureCelcius()) {
LOG.info("Threshold exceeded: current temperature " + ws.getCurrentTemperatureCelcius()
+ "C greater than threshold of " + maxTempThresholdInCelcius + "C");
return true;
}
if (minTempThresholdInCelcius >= ws.getCurrentTemperatureCelcius()) {
LOG.info("Threshold exceeded: current temperature " + ws.getCurrentTemperatureCelcius()
+ "C less than threshold of " + minTempThresholdInCelcius + "C");
return true;
}
if (rainDaysToLookBack > 0) {
if (totalRainAmountThresholdInMilliMetres <= ws
.getLastXDaysRainfallMilliLitres(rainDaysToLookBack)) {
LOG.info("Threshold exceeded: last " + rainDaysToLookBack + " days rainfall "
+ ws.getLastXDaysRainfallMilliLitres(rainDaysToLookBack)
+ "mm greater than threshold of " + totalRainAmountThresholdInMilliMetres + "mm");
return true;
}
}
if (rainDaysToLookAhead > 0) {
if (popThreshold <= ws.getNextXDaysPercentageOfPrecipitation(rainDaysToLookAhead)) {
LOG.info("Threshold exceeded: next " + rainDaysToLookAhead + " days PoP "
+ ws.getNextXDaysPercentageOfPrecipitation(rainDaysToLookAhead)
+ "% greater than threshold of " + popThreshold + "%");
return true;
}
}
}
return false;
}
/**
* Determines whether or not it is appropriate to irrigate based on the watering_days,
* watering_start_time and watering_threshold values in the configuration. Used by the
* {@code #startIrrigator()} worker thread.
*
* @return true to irrigate.
*/
protected boolean timeToIrrigate() {
final LocalTime lt = new LocalTime();
for (final int dayOfWeek : wateringDays) {
if (dayOfWeek == (lt.toDateTimeToday().getDayOfWeek())) {
if (wateringStartTime.getHourOfDay() == lt.getHourOfDay()) {
if (wateringStartTime.getMinuteOfHour() == lt.getMinuteOfHour()) {
LOG.info("It's time to irrigate!");
if (thresholdsExceeded()) {
LOG.info("Irrigation cancelled due to threshold being exceeded");
return false;
} else {
LOG.info("Thresholds all ok");
return true;
}
}
}
}
}
return false;
}
public void processWeatherMultiplierConfiguration() throws ConfigurationException {
final String NO_WEATHER_STATION = "ERROR: cannot have weather_multiplier values without a weatherstation configured";
// If any of the multiplier values are configured then try and process a multiplier
// configuration.
if ((config.getProperty("weather_multiplier_value") != null)
|| (config.getProperty("weather_multiplier_max_temp") != null)
|| (config.getProperty("weather_multiplier_days_to_look_ahead") != null)) {
// Must have a weather station to look at weather values
if (weatherStations.isEmpty()) {
throw new ConfigurationException(NO_WEATHER_STATION);
}
// Check that ALL values have been populated for the weather multiplier.
try {
weatherMultiplierValue = Math.abs(Double.parseDouble(config
.getProperty("weather_multiplier_value")));
} catch (final NumberFormatException e) {
throw new ConfigurationException(
"ERROR: must supply a valid weather_multiplier_value value");
}
try {
final String maxTemp = config.getProperty("weather_multiplier_max_temp");
final String delimiter = maxTemp.substring(maxTemp.length() - 1);
maxTempThresholdInCelcius = Double.parseDouble(maxTemp.substring(0, maxTemp.length() - 1));
if (delimiter.equalsIgnoreCase("F")) {
maxTempThresholdInCelcius = (maxTempThresholdInCelcius - 32) * (5.0 / 9.0);
} else if (!delimiter.equalsIgnoreCase("C")) {
throw new ConfigurationException(
"ERROR: must supply a delimiter of \"C\" or \"F\" to weather_multiplier_max_temp");
}
weatherMultiplierMaxTemp = maxTempThresholdInCelcius;
} catch (final NumberFormatException e) {
throw new ConfigurationException(
"ERROR: must supply a valid weather_multiplier_max_temp value");
}
try {
weatherMultiplierDaysToLookAhead = Math.abs(Integer.parseInt(config
.getProperty("weather_multiplier_days_to_look_ahead")));
} catch (final NumberFormatException e) {
throw new ConfigurationException(
"ERROR: must supply a valid weather_multiplier_days_to_look_ahead value");
}
}
}
public double getWeatherMultiplier() {
for (final WeatherStation ws : weatherStations) {
if (ws.getNextXDaysMaxTempCelcius(weatherMultiplierDaysToLookAhead) >= weatherMultiplierMaxTemp) {
LOG.info("Weather multiplier of " + weatherMultiplierValue
+ " triggered due to max temp of "
+ ws.getNextXDaysMaxTempCelcius(weatherMultiplierDaysToLookAhead) + "C over the next "
+ weatherMultiplierDaysToLookAhead + " days.");
return weatherMultiplierValue;
}
}
return 1.0;
}
public double getWeatherMultiplierMaxTemp() {
return this.weatherMultiplierMaxTemp;
}
public double getWeatherMultiplierValue() {
return weatherMultiplierValue;
}
public int getWeatherMultiplierDaysToLookAhead() {
return weatherMultiplierDaysToLookAhead;
}
private void startWeatherWatcher() {
final Thread purger = new WeatherWatcher();
purger.setName("WeatherWatcher");
purger.setPriority(Thread.MIN_PRIORITY);
purger.start();
}
/**
* Helper thread to periodically check if weather thresholds have been
* exceeded and if true then stop any active irrigation. Requires at least one
* valid valid {@link WeatherStation} to do anything.
*
*/
private class WeatherWatcher extends Thread {
@Override
public void run() {
final int THREAD_SLEEP = 60000; // Check every 1 minute.
for (;;) {
try {
Thread.sleep(THREAD_SLEEP);
// No point checking if we have no weather stations.
if (!getWeatherStations().isEmpty()) {
if(thresholdsExceeded()) {
for(final Controller c : getControllers()) {
if(c.isIrrigating()) {
LOG.info("Threshold exceeded, stopping active irrigation for Controller: "
+ c.getName());
c.stopIrrigation();
}
}
}
}
} catch (final Exception e) {
LOG.error("WeatherWatcher: " + e.getMessage());
}
}
}
}
}