package org.bensteele.jirrigate.controller; import java.io.IOException; import java.net.InetAddress; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.http.client.ClientProtocolException; import org.apache.log4j.Logger; import org.bensteele.jirrigate.controller.IrrigationResult.Result; import org.bensteele.jirrigate.controller.zone.Zone; /** * An EtherRain8 implementation of a {@link Controller}. * <p> * {@link http://www.quicksmart.com/qs_etherrain.html} * * @author Ben Steele (ben@bensteele.org) */ public class EtherRain8Controller extends Controller { /** * <pre> * ER - Command had a formatting error. * NA - Command came from an unauthorized IP address. * OK - Command was accepted. * </pre> */ private enum CommandStatus { ER("Command had a formatting error"), NA("Command came from an unauthorized IP address"), OK( "Command was accepted"); private final String text; private CommandStatus(final String text) { this.text = text; } @Override public String toString() { return text; } } /** * <pre> * NC - Irrigation did not complete. * OK - Irrigation completed with no issues. * RN - Irrigation was interrupted by the rain indicator. * SH - Irrigation was interrupted by the detection of a short in the valve wiring. * </pre> */ private enum OperatingResult { NC("Irrigation did not complete"), OK("Irrigation completed with no issues"), RN( "Irrigation was interrupted by the rain indicator"), SH( "Irrigation was interrupted by the detection of a short in the valve wiring"); private final String text; private OperatingResult(final String text) { this.text = text; } @Override public String toString() { return text; } } /** * <pre> * BZ - Device is busy. * RD - Device is ready. * WT - Device is waiting(delay before start). * </pre> */ private enum OperatingStatus { BZ("Device is busy"), RD("Device is ready"), WT("Device is waiting(delay before start)"); private final String text; private OperatingStatus(final String text) { this.text = text; } @Override public String toString() { return text; } } private final AtomicBoolean irrigationStopRequest = new AtomicBoolean(); private final ExecutorService executor = Executors.newSingleThreadExecutor(); private IrrigationResult result; public EtherRain8Controller(String controllerName, InetAddress ipAddress, int port, String username, String password, Logger log) throws IOException { super(controllerName, ipAddress, port, username, password, ControllerType.ETHERRAIN8, log); } private void addIrrigationResult(IrrigationResult result) { this.getIrrigationResults().add(result); } private String generateIrrigationUrl(IrrigationRequest request) { long valve1 = 0; long valve2 = 0; long valve3 = 0; long valve4 = 0; long valve5 = 0; long valve6 = 0; long valve7 = 0; long valve8 = 0; for (Zone z : request.getZones().keySet()) { // EtherRain8 takes minutes by default, convert from seconds. if (z.getId().matches("1")) { valve1 = request.getZones().get(z) / 60; } else if (z.getId().matches("2")) { valve2 = request.getZones().get(z) / 60; } else if (z.getId().matches("3")) { valve3 = request.getZones().get(z) / 60; } else if (z.getId().matches("4")) { valve4 = request.getZones().get(z) / 60; } else if (z.getId().matches("5")) { valve5 = request.getZones().get(z) / 60; } else if (z.getId().matches("6")) { valve6 = request.getZones().get(z) / 60; } else if (z.getId().matches("7")) { valve7 = request.getZones().get(z) / 60; } else if (z.getId().matches("8")) { valve8 = request.getZones().get(z) / 60; } } String startIrrigationUrl = getUrlPrefix() + "/result.cgi?xi="; return startIrrigationUrl + "0:" + valve1 + ":" + valve2 + ":" + valve3 + ":" + valve4 + ":" + valve5 + ":" + valve6 + ":" + valve7 + ":" + valve8; } private CommandStatus getCommandStatus(List<String> response) throws ClientProtocolException, IOException { for (String s : response) { // cs = CommandStatus if (s.contains("cs: ")) { if (s.contains("ER")) { return CommandStatus.ER; } else if (s.contains("NA")) { return CommandStatus.NA; } else if (s.contains("OK")) { return CommandStatus.OK; } } } return null; } @Override protected Logger getLog() { return super.getLog(); } private String getLoginUrl() { return getUrlPrefix() + "/ergetcfg.cgi?lu=" + this.getUsername() + "&lp=" + this.getPassword(); } private OperatingResult getOperatingResult() throws ClientProtocolException, IOException { login(); String getStatusUrl = getUrlPrefix() + "/result.cgi?xs"; List<String> response = this.sendHttpGet(getStatusUrl); for (String s : response) { // rz = OperatingResult if (s.contains("rz: ")) { if (s.contains("NC")) { return OperatingResult.NC; } else if (s.contains("OK")) { return OperatingResult.OK; } else if (s.contains("RN")) { return OperatingResult.RN; } else if (s.contains("SH")) { return OperatingResult.SH; } } } return null; } private OperatingStatus getOperatingStatus() throws ClientProtocolException, IOException { login(); String getStatusUrl = getUrlPrefix() + "/result.cgi?xs"; List<String> response = this.sendHttpGet(getStatusUrl); for (String s : response) { // os = OperatingStatus if (s.contains("os: ")) { if (s.contains("BZ")) { return OperatingStatus.BZ; } else if (s.contains("RD")) { return OperatingStatus.RD; } else if (s.contains("WT")) { return OperatingStatus.WT; } } } return null; } @Override public String getStatus() { try { if (login()) { return getOperatingStatus().toString(); } } catch (ClientProtocolException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return "ERROR: Could not login to controller"; } private String getUrlPrefix() { return "http://" + this.getIpAddress().getHostAddress() + ":" + this.getPort(); } private boolean login() throws ClientProtocolException, IOException { List<String> response = this.sendHttpGet(getLoginUrl()); for (String s : response) { // ur will be set to username if succesfully logged in. if (s.contains("ur: ")) { if (s.contains(this.getUsername())) { return true; } } } return false; } @Override public void startIrrigation(final IrrigationRequest request) { executor.execute(new Runnable() { @Override public void run() { try { // Reset any previous results. result = new IrrigationResult(null, null, 0); long startTime = System.currentTimeMillis(); Set<Zone> irrigatedZones = stripNoIrrigationZones(request); if (!login()) { setIsIrrigating(false); result = new IrrigationResult("No irrigation command sent", irrigatedZones, startTime); result.setResult(Result.FAIL); result.setMessage("ERROR: Login failed"); result.setEndTime(System.currentTimeMillis()); addIrrigationResult(result); getLog().info("Irrigation Result: \n" + result.toString()); return; } OperatingStatus os = getOperatingStatus(); if (os != OperatingStatus.RD) { setIsIrrigating(false); result = new IrrigationResult("No irrigation command sent", irrigatedZones, startTime); result.setResult(Result.FAIL); result.setMessage("ERROR: " + os.toString()); result.setEndTime(System.currentTimeMillis()); addIrrigationResult(result); getLog().info("Irrigation Result: \n" + result.toString()); return; } String commandSent = generateIrrigationUrl(request); startTime = System.currentTimeMillis(); List<String> response = sendHttpGet(commandSent); CommandStatus cs = getCommandStatus(response); result = new IrrigationResult(commandSent, irrigatedZones, startTime); if (cs == CommandStatus.OK) { while (true) { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } if (irrigationStopRequest.get()) { irrigationStopRequest.set(false); setIsIrrigating(false); result.setResult(Result.INTERRUPTED); result.setMessage("INFO: Irrigation interrupted by stop request"); result.setZones(irrigatedZones); result.setEndTime(System.currentTimeMillis()); addIrrigationResult(result); getLog().info("Irrigation Result: \n" + result.toString()); return; } if (getOperatingStatus() == OperatingStatus.RD) { // Irrigation finished. break; } } } else { setIsIrrigating(false); result.setResult(Result.FAIL); result.setMessage("ERROR: " + cs.toString()); result.setEndTime(System.currentTimeMillis()); addIrrigationResult(result); getLog().info("Irrigation Result: \n" + result.toString()); return; } OperatingResult or = getOperatingResult(); if (or != OperatingResult.OK) { result.setResult(Result.FAIL); result.setMessage("ERROR: " + or.toString()); } else { result.setResult(Result.SUCCESS); result.setMessage("INFO: " + or.toString()); } result.setZones(irrigatedZones); result.setEndTime(System.currentTimeMillis()); setIsIrrigating(false); addIrrigationResult(result); getLog().info("Irrigation Result: \n" + result.toString()); } catch (ClientProtocolException e) { getLog().error(e.getMessage()); } catch (IOException e) { getLog().error(e.getMessage()); } finally { setIsIrrigating(false); } } }); } @Override public boolean stopIrrigation() { getLog().info("Stop request received for " + this.getName()); // If it's not currently Irrigating then there is nothing to stop. if (!isIrrigating()) { getLog().warn(this.getName() + " not currently irrigating, aborting stop request"); return true; } irrigationStopRequest.set(true); while (irrigationStopRequest.get()) { // Wait for the Thread running the irrigation on this controller to acknowledge and set this // to false. } List<String> response; try { if (!login()) { return false; } String stopIrrigationUrl = getUrlPrefix() + "/result.cgi?xr"; response = this.sendHttpGet(stopIrrigationUrl); if (getCommandStatus(response) == CommandStatus.OK) { getLog().info("Stop request successful for " + this.getName()); return true; } } catch (ClientProtocolException e) { getLog().error(e.getMessage()); } catch (IOException e) { getLog().error(e.getMessage()); } getLog().info( "Stop request unsuccessful for " + this.getName() + " controller says: " + getStatus()); return false; } }