2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.etherrain.internal.api;
15 import java.io.BufferedReader;
16 import java.io.IOException;
17 import java.io.StringReader;
18 import java.net.DatagramPacket;
19 import java.net.DatagramSocket;
20 import java.net.HttpURLConnection;
21 import java.net.InetAddress;
22 import java.net.SocketTimeoutException;
23 import java.util.Collections;
24 import java.util.LinkedList;
25 import java.util.List;
26 import java.util.concurrent.ExecutionException;
27 import java.util.concurrent.TimeUnit;
28 import java.util.concurrent.TimeoutException;
29 import java.util.regex.Matcher;
30 import java.util.regex.Pattern;
31 import java.util.stream.Collectors;
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jetty.client.HttpClient;
35 import org.eclipse.jetty.client.api.ContentResponse;
36 import org.openhab.binding.etherrain.internal.EtherRainException;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
41 * The {@link EtherRainCommunication} handles communication with EtherRain
42 * controllers so that the API is all in one place
44 * @author Joe Inkenbrandt - Initial contribution
48 public class EtherRainCommunication {
50 private static final String BROADCAST_DISCOVERY_MESSAGE = "eviro_id_request1";
51 private static final int BROADCAST_DISCOVERY_PORT = 8089;
53 private static final String ETHERRAIN_USERNAME = "admin";
55 private static final int HTTP_TIMEOUT = 3;
57 private static final int BROADCAST_TIMEOUT = 80;
59 private static final String RESPONSE_STATUS_PATTERN = "^\\s*(un|ma|ac|os|cs|rz|ri|rn):\\s*([a-zA-Z0-9\\.]*)(\\s*<br>)?";
60 private static final String BROADCAST_RESPONSE_DISCOVER_PATTERN = "eviro t=(\\S*) n=(\\S*) p=(\\S*) a=(\\S*)";
62 private static final Pattern broadcastResponseDiscoverPattern = Pattern
63 .compile(BROADCAST_RESPONSE_DISCOVER_PATTERN);
64 private static final Pattern responseStatusPattern = Pattern.compile(RESPONSE_STATUS_PATTERN);
66 private final Logger logger = LoggerFactory.getLogger(EtherRainCommunication.class);
67 private final HttpClient httpClient;
69 private final String address;
70 private final int port;
71 private final String password;
73 public EtherRainCommunication(String address, int port, String password, HttpClient httpClient) {
74 this.address = address;
76 this.password = password;
77 this.httpClient = httpClient;
80 public static List<EtherRainUdpResponse> autoDiscover() {
81 return updBroadcast();
84 public EtherRainStatusResponse commandStatus() throws EtherRainException, IOException {
87 List<String> responseList = sendGet("result.cgi?xs");
89 if (responseList.isEmpty()) {
90 throw new EtherRainException("Empty Response");
93 EtherRainStatusResponse response = new EtherRainStatusResponse();
95 for (String line : responseList) {
97 Matcher m = responseStatusPattern.matcher(line);
100 String command = m.replaceAll("$1");
101 String status = m.replaceAll("$2");
105 response.setUniqueName(status);
108 response.setMacAddress(status);
111 response.setServiceAccount(status);
114 response.setOperatingStatus(EtherRainOperatingStatus.valueOf(status.toUpperCase()));
117 response.setLastCommandStatus(EtherRainCommandStatus.valueOf(status.toUpperCase()));
120 response.setLastCommandResult(EtherRainCommandResult.valueOf(status.toUpperCase()));
123 response.setLastActiveValue(Integer.parseInt(status));
126 response.setRainSensor(Integer.parseInt(status) == 1);
129 logger.debug("Unknown response: {}", command);
137 public synchronized boolean commandIrrigate(int delay, int zone1, int zone2, int zone3, int zone4, int zone5,
138 int zone6, int zone7, int zone8) {
140 sendGet("result.cgi?xi=" + delay + ":" + zone1 + ":" + zone2 + ":" + zone3 + ":" + zone4 + ":" + zone5 + ":"
141 + zone6 + ":" + zone7 + ":" + zone8);
142 } catch (IOException e) {
143 logger.warn("Could not send irrigate command to etherrain: {}", e.getMessage());
150 public synchronized boolean commandClear() {
152 sendGet("/result.cgi?xr");
153 } catch (IOException e) {
154 logger.warn("Could not send clear command to etherrain: {}", e.getMessage());
161 public synchronized boolean commandLogin() throws EtherRainException {
163 sendGet("/ergetcfg.cgi?lu=" + ETHERRAIN_USERNAME + "&lp=" + password);
164 } catch (IOException e) {
165 logger.warn("Could not send clear command to etherrain: {}", e.getMessage());
172 public synchronized boolean commandLogout() {
174 sendGet("/ergetcfg.cgi?m=o");
175 } catch (IOException e) {
176 logger.warn("Could not send logout command to etherrain: {}", e.getMessage());
183 private synchronized List<String> sendGet(String command) throws IOException {
184 String url = "http://" + address + ":" + port + "/" + command;
186 ContentResponse response;
189 response = httpClient.newRequest(url).timeout(HTTP_TIMEOUT, TimeUnit.SECONDS).send();
190 if (response.getStatus() != HttpURLConnection.HTTP_OK) {
191 logger.warn("Etherrain return status other than HTTP_OK : {}", response.getStatus());
192 return Collections.emptyList();
194 } catch (TimeoutException | ExecutionException e) {
195 logger.warn("Could not connect to Etherrain with exception: {}", e.getMessage());
196 return Collections.emptyList();
197 } catch (InterruptedException e) {
198 logger.warn("Connect to Etherrain interrupted: {}", e.getMessage());
199 Thread.currentThread().interrupt();
200 return Collections.emptyList();
203 return new BufferedReader(new StringReader(response.getContentAsString())).lines().collect(Collectors.toList());
206 private static List<EtherRainUdpResponse> updBroadcast() {
207 List<EtherRainUdpResponse> rList = new LinkedList<>();
209 // Find the server using UDP broadcast
211 try (DatagramSocket c = new DatagramSocket()) {
212 c.setSoTimeout(BROADCAST_TIMEOUT);
213 c.setBroadcast(true);
215 byte[] sendData = BROADCAST_DISCOVERY_MESSAGE.getBytes("UTF-8");
217 // Try the 255.255.255.255 first
218 DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length,
219 InetAddress.getByName("255.255.255.255"), BROADCAST_DISCOVERY_PORT);
223 // Wait for a response
224 byte[] recvBuf = new byte[15000];
225 DatagramPacket receivePacket;
227 receivePacket = new DatagramPacket(recvBuf, recvBuf.length);
228 c.receive(receivePacket);
229 } catch (SocketTimeoutException e) {
233 // Check if the message is correct
234 String message = new String(receivePacket.getData(), "UTF-8").trim();
236 if (message.length() == 0) {
240 String addressBC = receivePacket.getAddress().getHostAddress();
242 Matcher matcher = broadcastResponseDiscoverPattern.matcher(message);
243 String deviceTypeBC = matcher.replaceAll("$1");
244 String unqiueNameBC = matcher.replaceAll("$2");
245 int portBC = Integer.parseInt(matcher.replaceAll("$3"));
247 // NOTE: Version 3.77 of API states that Additional Parameters (a=) are not used
249 rList.add(new EtherRainUdpResponse(deviceTypeBC, addressBC, portBC, unqiueNameBC, ""));
251 } catch (IOException ex) {