2 * Copyright (c) 2010-2021 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.haywardomnilogic.internal.handler;
15 import java.io.StringReader;
16 import java.util.ArrayList;
17 import java.util.Collection;
18 import java.util.Collections;
19 import java.util.List;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24 import java.util.concurrent.TimeoutException;
26 import javax.xml.xpath.XPath;
27 import javax.xml.xpath.XPathConstants;
28 import javax.xml.xpath.XPathExpressionException;
29 import javax.xml.xpath.XPathFactory;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.eclipse.jetty.client.HttpClient;
34 import org.eclipse.jetty.client.api.ContentResponse;
35 import org.eclipse.jetty.client.api.Request;
36 import org.eclipse.jetty.client.util.StringContentProvider;
37 import org.eclipse.jetty.http.HttpHeader;
38 import org.eclipse.jetty.http.HttpMethod;
39 import org.eclipse.jetty.http.HttpVersion;
40 import org.openhab.binding.haywardomnilogic.internal.HaywardAccount;
41 import org.openhab.binding.haywardomnilogic.internal.HaywardBindingConstants;
42 import org.openhab.binding.haywardomnilogic.internal.HaywardException;
43 import org.openhab.binding.haywardomnilogic.internal.HaywardThingHandler;
44 import org.openhab.binding.haywardomnilogic.internal.HaywardTypeToRequest;
45 import org.openhab.binding.haywardomnilogic.internal.config.HaywardConfig;
46 import org.openhab.binding.haywardomnilogic.internal.discovery.HaywardDiscoveryService;
47 import org.openhab.core.library.types.OnOffType;
48 import org.openhab.core.thing.Bridge;
49 import org.openhab.core.thing.ChannelUID;
50 import org.openhab.core.thing.Thing;
51 import org.openhab.core.thing.ThingStatus;
52 import org.openhab.core.thing.ThingStatusDetail;
53 import org.openhab.core.thing.binding.BaseBridgeHandler;
54 import org.openhab.core.thing.binding.ThingHandlerService;
55 import org.openhab.core.types.Command;
56 import org.slf4j.Logger;
57 import org.slf4j.LoggerFactory;
58 import org.w3c.dom.NodeList;
59 import org.xml.sax.InputSource;
62 * The {@link HaywardBridgeHandler} is responsible for handling commands, which are
63 * sent to one of the channels.
65 * @author Matt Myers - Initial contribution
69 public class HaywardBridgeHandler extends BaseBridgeHandler {
70 private final Logger logger = LoggerFactory.getLogger(HaywardBridgeHandler.class);
71 private final HttpClient httpClient;
72 private @Nullable ScheduledFuture<?> initializeFuture;
73 private @Nullable ScheduledFuture<?> pollTelemetryFuture;
74 private @Nullable ScheduledFuture<?> pollAlarmsFuture;
75 private int commFailureCount;
76 public HaywardConfig config = getConfig().as(HaywardConfig.class);
77 public HaywardAccount account = getConfig().as(HaywardAccount.class);
80 public Collection<Class<? extends ThingHandlerService>> getServices() {
81 return Collections.singleton(HaywardDiscoveryService.class);
84 public HaywardBridgeHandler(Bridge bridge, HttpClient httpClient) {
86 this.httpClient = httpClient;
90 public void handleCommand(ChannelUID channelUID, Command command) {
94 public void dispose() {
95 clearPolling(initializeFuture);
96 clearPolling(pollTelemetryFuture);
97 clearPolling(pollAlarmsFuture);
98 logger.trace("Hayward polling cancelled");
103 public void initialize() {
104 initializeFuture = scheduler.schedule(this::scheduledInitialize, 1, TimeUnit.SECONDS);
108 public void scheduledInitialize() {
109 config = getConfigAs(HaywardConfig.class);
112 clearPolling(pollTelemetryFuture);
113 clearPolling(pollAlarmsFuture);
116 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
117 "Unable to Login to Hayward's server");
118 clearPolling(pollTelemetryFuture);
119 clearPolling(pollAlarmsFuture);
120 commFailureCount = 50;
125 if (!(getSiteList())) {
126 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
127 "Unable to getMSP from Hayward's server");
128 clearPolling(pollTelemetryFuture);
129 clearPolling(pollAlarmsFuture);
130 commFailureCount = 50;
135 if (!(mspConfigUnits())) {
136 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
137 "Unable to getMSPConfigUnits from Hayward's server");
138 clearPolling(pollTelemetryFuture);
139 clearPolling(pollAlarmsFuture);
140 commFailureCount = 50;
145 if (this.thing.getStatus() != ThingStatus.ONLINE) {
146 updateStatus(ThingStatus.ONLINE);
149 logger.debug("Succesfully opened connection to Hayward's server: {} Username:{}", config.endpointUrl,
153 logger.trace("Hayward Telemetry polling scheduled");
155 if (config.alarmPollTime > 0) {
158 } catch (HaywardException e) {
159 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR,
160 "scheduledInitialize exception: " + e.getMessage());
161 clearPolling(pollTelemetryFuture);
162 clearPolling(pollAlarmsFuture);
163 commFailureCount = 50;
166 } catch (InterruptedException e) {
171 public synchronized boolean login() throws HaywardException, InterruptedException {
175 // *****Login to Hayward server
176 String urlParameters = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Request>" + "<Name>Login</Name><Parameters>"
177 + "<Parameter name=\"UserName\" dataType=\"String\">" + config.username + "</Parameter>"
178 + "<Parameter name=\"Password\" dataType=\"String\">" + config.password + "</Parameter>"
179 + "</Parameters></Request>";
181 xmlResponse = httpXmlResponse(urlParameters);
183 if (xmlResponse.isEmpty()) {
187 status = evaluateXPath("/Response/Parameters//Parameter[@name='Status']/text()", xmlResponse).get(0);
189 if (!("0".equals(status))) {
190 logger.debug("Hayward Connection thing: Login XML response: {}", xmlResponse);
194 account.token = evaluateXPath("/Response/Parameters//Parameter[@name='Token']/text()", xmlResponse).get(0);
195 account.userID = evaluateXPath("/Response/Parameters//Parameter[@name='UserID']/text()", xmlResponse).get(0);
199 public synchronized boolean getApiDef() throws HaywardException, InterruptedException {
202 // *****getConfig from Hayward server
203 String urlParameters = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Request><Name>GetAPIDef</Name><Parameters>"
204 + "<Parameter name=\"Token\" dataType=\"String\">" + account.token + "</Parameter>"
205 + "<Parameter name=\"MspSystemID\" dataType=\"int\">" + account.mspSystemID + "</Parameter>;"
206 + "<Parameter name=\"Version\" dataType=\"string\">0.4</Parameter >\r\n"
207 + "<Parameter name=\"Language\" dataType=\"string\">en</Parameter >\r\n" + "</Parameters></Request>";
209 xmlResponse = httpXmlResponse(urlParameters);
211 if (xmlResponse.isEmpty()) {
212 logger.debug("Hayward Connection thing: Login XML response was null");
218 public synchronized boolean getSiteList() throws HaywardException, InterruptedException {
223 String urlParameters = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Request><Name>GetSiteList</Name><Parameters>"
224 + "<Parameter name=\"Token\" dataType=\"String\">" + account.token
225 + "</Parameter><Parameter name=\"UserID\" dataType=\"String\">" + account.userID
226 + "</Parameter></Parameters></Request>";
228 xmlResponse = httpXmlResponse(urlParameters);
230 if (xmlResponse.isEmpty()) {
231 logger.debug("Hayward Connection thing: getSiteList XML response was null");
235 status = evaluateXPath("/Response/Parameters//Parameter[@name='Status']/text()", xmlResponse).get(0);
237 if (!("0".equals(status))) {
238 logger.debug("Hayward Connection thing: getSiteList XML response: {}", xmlResponse);
242 account.mspSystemID = evaluateXPath("/Response/Parameters/Parameter/Item//Property[@name='MspSystemID']/text()",
244 account.backyardName = evaluateXPath(
245 "/Response/Parameters/Parameter/Item//Property[@name='BackyardName']/text()", xmlResponse).get(0);
246 account.address = evaluateXPath("/Response/Parameters/Parameter/Item//Property[@name='Address']/text()",
251 public synchronized String getMspConfig() throws HaywardException, InterruptedException {
252 // *****getMspConfig from Hayward server
253 String urlParameters = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Request><Name>GetMspConfigFile</Name><Parameters>"
254 + "<Parameter name=\"Token\" dataType=\"String\">" + account.token + "</Parameter>"
255 + "<Parameter name=\"MspSystemID\" dataType=\"int\">" + account.mspSystemID
256 + "</Parameter><Parameter name=\"Version\" dataType=\"string\">0</Parameter>\r\n"
257 + "</Parameters></Request>";
259 String xmlResponse = httpXmlResponse(urlParameters);
261 // Debug: Inject xml file for testing
263 // "C:/Users/Controls/openhab-2-5-x/git/openhab-addons/bundles/org.openhab.binding.haywardomnilogic/getConfig.xml";
264 // xmlResponse = new String(Files.readAllBytes(Paths.get(path)));
266 if (xmlResponse.isEmpty()) {
267 logger.debug("Hayward Connection thing: requestConfig XML response was null");
271 if (evaluateXPath("//Backyard/Name/text()", xmlResponse).isEmpty()) {
272 logger.debug("Hayward Connection thing: requestConfiguration XML response: {}", xmlResponse);
278 public synchronized boolean mspConfigUnits() throws HaywardException, InterruptedException {
279 List<String> property1 = new ArrayList<>();
280 List<String> property2 = new ArrayList<>();
282 String xmlResponse = getMspConfig();
284 // Get Units (Standard, Metric)
285 property1 = evaluateXPath("//System/Units/text()", xmlResponse);
286 account.units = property1.get(0);
288 // Get Variable Speed Pump Units (percent, RPM)
289 property2 = evaluateXPath("//System/Msp-Vsp-Speed-Format/text()", xmlResponse);
290 account.vspSpeedFormat = property2.get(0);
295 public synchronized boolean getTelemetryData() throws HaywardException, InterruptedException {
296 // *****getTelemetry from Hayward server
297 String urlParameters = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Request><Name>GetTelemetryData</Name><Parameters>"
298 + "<Parameter name=\"Token\" dataType=\"String\">" + account.token + "</Parameter>"
299 + "<Parameter name=\"MspSystemID\" dataType=\"int\">" + account.mspSystemID
300 + "</Parameter></Parameters></Request>";
302 String xmlResponse = httpXmlResponse(urlParameters);
304 if (xmlResponse.isEmpty()) {
305 logger.debug("Hayward Connection thing: getTelemetry XML response was null");
309 if (!evaluateXPath("/Response/Parameters//Parameter[@name='StatusMessage']/text()", xmlResponse).isEmpty()) {
310 logger.debug("Hayward Connection thing: getTelemetry XML response: {}", xmlResponse);
314 for (Thing thing : getThing().getThings()) {
315 if (thing.getHandler() instanceof HaywardThingHandler) {
316 HaywardThingHandler handler = (HaywardThingHandler) thing.getHandler();
317 if (handler != null) {
318 handler.getTelemetry(xmlResponse);
325 public synchronized boolean getAlarmList() throws HaywardException {
326 for (Thing thing : getThing().getThings()) {
327 Map<String, String> properties = thing.getProperties();
328 if ("BACKYARD".equals(properties.get(HaywardBindingConstants.PROPERTY_TYPE))) {
329 HaywardBackyardHandler handler = (HaywardBackyardHandler) thing.getHandler();
330 if (handler != null) {
331 String systemID = properties.get(HaywardBindingConstants.PROPERTY_SYSTEM_ID);
332 if (systemID != null) {
333 return handler.getAlarmList(systemID);
341 private synchronized void initPolling(int initalDelay) {
342 pollTelemetryFuture = scheduler.scheduleWithFixedDelay(() -> {
344 if (commFailureCount >= 5) {
345 commFailureCount = 0;
346 clearPolling(pollTelemetryFuture);
347 clearPolling(pollAlarmsFuture);
351 if (!(getTelemetryData())) {
355 updateStatus(ThingStatus.ONLINE);
356 } catch (HaywardException e) {
357 logger.debug("Hayward Connection thing: Exception during poll: {}", e.getMessage());
358 } catch (InterruptedException e) {
361 }, initalDelay, config.telemetryPollTime, TimeUnit.SECONDS);
365 private synchronized void initAlarmPolling(int initalDelay) {
366 pollAlarmsFuture = scheduler.scheduleWithFixedDelay(() -> {
369 } catch (HaywardException e) {
370 logger.debug("Hayward Connection thing: Exception during poll: {}", e.getMessage());
372 }, initalDelay, config.alarmPollTime, TimeUnit.SECONDS);
375 private void clearPolling(@Nullable ScheduledFuture<?> pollJob) {
376 if (pollJob != null) {
377 pollJob.cancel(false);
382 Thing getThingForType(HaywardTypeToRequest type, int num) {
383 for (Thing thing : getThing().getThings()) {
384 Map<String, String> properties = thing.getProperties();
385 if (Integer.toString(num).equals(properties.get(HaywardBindingConstants.PROPERTY_SYSTEM_ID))) {
386 if (type.toString().equals(properties.get(HaywardBindingConstants.PROPERTY_TYPE))) {
394 public List<String> evaluateXPath(String xpathExp, String xmlResponse) {
395 List<String> values = new ArrayList<>();
397 InputSource inputXML = new InputSource(new StringReader(xmlResponse));
398 XPath xPath = XPathFactory.newInstance().newXPath();
399 NodeList nodes = (NodeList) xPath.evaluate(xpathExp, inputXML, XPathConstants.NODESET);
401 for (int i = 0; i < nodes.getLength(); i++) {
402 values.add(nodes.item(i).getNodeValue());
404 } catch (XPathExpressionException e) {
405 logger.warn("XPathExpression exception: {}", e.getMessage());
410 private Request sendRequestBuilder(String url, HttpMethod method) {
411 return this.httpClient.newRequest(url).agent("NextGenForIPhone/16565 CFNetwork/887 Darwin/17.0.0")
412 .method(method).header(HttpHeader.ACCEPT_LANGUAGE, "en-us").header(HttpHeader.ACCEPT, "*/*")
413 .header(HttpHeader.ACCEPT_ENCODING, "gzip, deflate").version(HttpVersion.HTTP_1_1)
414 .header(HttpHeader.CONNECTION, "keep-alive").header(HttpHeader.HOST, "www.haywardomnilogic.com:80")
415 .timeout(10, TimeUnit.SECONDS);
418 public synchronized String httpXmlResponse(String urlParameters) throws HaywardException, InterruptedException {
419 String urlParameterslength = Integer.toString(urlParameters.length());
420 String statusMessage;
423 ContentResponse httpResponse = sendRequestBuilder(config.endpointUrl, HttpMethod.POST)
424 .content(new StringContentProvider(urlParameters), "text/xml; charset=utf-8")
425 .header(HttpHeader.CONTENT_LENGTH, urlParameterslength).send();
427 int status = httpResponse.getStatus();
428 String xmlResponse = httpResponse.getContentAsString();
431 List<String> statusMessages = evaluateXPath(
432 "/Response/Parameters//Parameter[@name='StatusMessage']/text()", xmlResponse);
433 if (!(statusMessages.isEmpty())) {
434 statusMessage = statusMessages.get(0);
436 statusMessage = httpResponse.getReason();
439 if (logger.isTraceEnabled()) {
440 logger.trace("Hayward Connection thing: {} Hayward http command: {}", getCallingMethod(),
442 logger.trace("Hayward Connection thing: {} Hayward http response: {} {}", getCallingMethod(),
443 statusMessage, xmlResponse);
447 if (logger.isDebugEnabled()) {
448 logger.debug("Hayward Connection thing: {} Hayward http command: {}", getCallingMethod(),
450 logger.debug("Hayward Connection thing: {} Hayward http response: {} {}", getCallingMethod(),
451 status, xmlResponse);
455 } catch (ExecutionException e) {
456 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
457 "Unable to resolve host. Check Hayward hostname and your internet connection. " + e);
459 } catch (TimeoutException e) {
460 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
461 "Connection Timeout. Check Hayward hostname and your internet connection. " + e);
466 private String getCallingMethod() {
467 StackTraceElement[] stacktrace = Thread.currentThread().getStackTrace();
468 StackTraceElement e = stacktrace[3];
469 return e.getMethodName();
472 public int convertCommand(Command command) {
473 if (command == OnOffType.ON) {