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 updateStatus(ThingStatus.UNKNOWN);
105 initializeFuture = scheduler.schedule(this::scheduledInitialize, 1, TimeUnit.SECONDS);
109 public void scheduledInitialize() {
110 config = getConfigAs(HaywardConfig.class);
113 clearPolling(pollTelemetryFuture);
114 clearPolling(pollAlarmsFuture);
117 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
118 "Unable to Login to Hayward's server");
119 clearPolling(pollTelemetryFuture);
120 clearPolling(pollAlarmsFuture);
121 commFailureCount = 50;
126 if (!(getSiteList())) {
127 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
128 "Unable to getMSP from Hayward's server");
129 clearPolling(pollTelemetryFuture);
130 clearPolling(pollAlarmsFuture);
131 commFailureCount = 50;
136 if (!(mspConfigUnits())) {
137 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
138 "Unable to getMSPConfigUnits from Hayward's server");
139 clearPolling(pollTelemetryFuture);
140 clearPolling(pollAlarmsFuture);
141 commFailureCount = 50;
146 updateStatus(ThingStatus.ONLINE);
147 logger.debug("Succesfully opened connection to Hayward's server: {} Username:{}", config.endpointUrl,
151 logger.trace("Hayward Telemetry polling scheduled");
153 if (config.alarmPollTime > 0) {
156 } catch (HaywardException e) {
157 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR,
158 "scheduledInitialize exception: " + e.getMessage());
159 clearPolling(pollTelemetryFuture);
160 clearPolling(pollAlarmsFuture);
161 commFailureCount = 50;
164 } catch (InterruptedException e) {
169 public synchronized boolean login() throws HaywardException, InterruptedException {
173 // *****Login to Hayward server
174 String urlParameters = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Request>" + "<Name>Login</Name><Parameters>"
175 + "<Parameter name=\"UserName\" dataType=\"String\">" + config.username + "</Parameter>"
176 + "<Parameter name=\"Password\" dataType=\"String\">" + config.password + "</Parameter>"
177 + "</Parameters></Request>";
179 xmlResponse = httpXmlResponse(urlParameters);
181 if (xmlResponse.isEmpty()) {
185 status = evaluateXPath("/Response/Parameters//Parameter[@name='Status']/text()", xmlResponse).get(0);
187 if (!(status.equals("0"))) {
188 logger.debug("Hayward Connection thing: Login XML response: {}", xmlResponse);
192 account.token = evaluateXPath("/Response/Parameters//Parameter[@name='Token']/text()", xmlResponse).get(0);
193 account.userID = evaluateXPath("/Response/Parameters//Parameter[@name='UserID']/text()", xmlResponse).get(0);
197 public synchronized boolean getApiDef() throws HaywardException, InterruptedException {
200 // *****getConfig from Hayward server
201 String urlParameters = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Request><Name>GetAPIDef</Name><Parameters>"
202 + "<Parameter name=\"Token\" dataType=\"String\">" + account.token + "</Parameter>"
203 + "<Parameter name=\"MspSystemID\" dataType=\"int\">" + account.mspSystemID + "</Parameter>;"
204 + "<Parameter name=\"Version\" dataType=\"string\">0.4</Parameter >\r\n"
205 + "<Parameter name=\"Language\" dataType=\"string\">en</Parameter >\r\n" + "</Parameters></Request>";
207 xmlResponse = httpXmlResponse(urlParameters);
209 if (xmlResponse.isEmpty()) {
210 logger.debug("Hayward Connection thing: Login XML response was null");
216 public synchronized boolean getSiteList() throws HaywardException, InterruptedException {
221 String urlParameters = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Request><Name>GetSiteList</Name><Parameters>"
222 + "<Parameter name=\"Token\" dataType=\"String\">" + account.token
223 + "</Parameter><Parameter name=\"UserID\" dataType=\"String\">" + account.userID
224 + "</Parameter></Parameters></Request>";
226 xmlResponse = httpXmlResponse(urlParameters);
228 if (xmlResponse.isEmpty()) {
229 logger.debug("Hayward Connection thing: getSiteList XML response was null");
233 status = evaluateXPath("/Response/Parameters//Parameter[@name='Status']/text()", xmlResponse).get(0);
235 if (!(status.equals("0"))) {
236 logger.debug("Hayward Connection thing: getSiteList XML response: {}", xmlResponse);
240 account.mspSystemID = evaluateXPath("/Response/Parameters/Parameter/Item//Property[@name='MspSystemID']/text()",
242 account.backyardName = evaluateXPath(
243 "/Response/Parameters/Parameter/Item//Property[@name='BackyardName']/text()", xmlResponse).get(0);
244 account.address = evaluateXPath("/Response/Parameters/Parameter/Item//Property[@name='Address']/text()",
249 public synchronized String getMspConfig() throws HaywardException, InterruptedException {
250 // *****getMspConfig from Hayward server
251 String urlParameters = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Request><Name>GetMspConfigFile</Name><Parameters>"
252 + "<Parameter name=\"Token\" dataType=\"String\">" + account.token + "</Parameter>"
253 + "<Parameter name=\"MspSystemID\" dataType=\"int\">" + account.mspSystemID
254 + "</Parameter><Parameter name=\"Version\" dataType=\"string\">0</Parameter>\r\n"
255 + "</Parameters></Request>";
257 String xmlResponse = httpXmlResponse(urlParameters);
259 // Debug: Inject xml file for testing
261 // "C:/Users/Controls/openhab-2-5-x/git/openhab-addons/bundles/org.openhab.binding.haywardomnilogic/getConfig.xml";
262 // xmlResponse = new String(Files.readAllBytes(Paths.get(path)));
264 if (xmlResponse.isEmpty()) {
265 logger.debug("Hayward Connection thing: requestConfig XML response was null");
269 if (evaluateXPath("//Backyard/Name/text()", xmlResponse).isEmpty()) {
270 logger.debug("Hayward Connection thing: requestConfiguration XML response: {}", xmlResponse);
276 public synchronized boolean mspConfigUnits() throws HaywardException, InterruptedException {
277 List<String> property1 = new ArrayList<>();
278 List<String> property2 = new ArrayList<>();
280 String xmlResponse = getMspConfig();
282 // Get Units (Standard, Metric)
283 property1 = evaluateXPath("//System/Units/text()", xmlResponse);
284 account.units = property1.get(0);
286 // Get Variable Speed Pump Units (percent, RPM)
287 property2 = evaluateXPath("//System/Msp-Vsp-Speed-Format/text()", xmlResponse);
288 account.vspSpeedFormat = property2.get(0);
293 public synchronized boolean getTelemetryData() throws HaywardException, InterruptedException {
294 // *****getTelemetry from Hayward server
295 String urlParameters = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Request><Name>GetTelemetryData</Name><Parameters>"
296 + "<Parameter name=\"Token\" dataType=\"String\">" + account.token + "</Parameter>"
297 + "<Parameter name=\"MspSystemID\" dataType=\"int\">" + account.mspSystemID
298 + "</Parameter></Parameters></Request>";
300 String xmlResponse = httpXmlResponse(urlParameters);
302 if (xmlResponse.isEmpty()) {
303 logger.debug("Hayward Connection thing: getTelemetry XML response was null");
307 if (!evaluateXPath("/Response/Parameters//Parameter[@name='StatusMessage']/text()", xmlResponse).isEmpty()) {
308 logger.debug("Hayward Connection thing: getTelemetry XML response: {}", xmlResponse);
312 for (Thing thing : getThing().getThings()) {
313 if (thing.getHandler() instanceof HaywardThingHandler) {
314 HaywardThingHandler handler = (HaywardThingHandler) thing.getHandler();
315 if (handler != null) {
316 handler.getTelemetry(xmlResponse);
323 public synchronized boolean getAlarmList() throws HaywardException {
324 for (Thing thing : getThing().getThings()) {
325 Map<String, String> properties = thing.getProperties();
326 if ("BACKYARD".equals(properties.get(HaywardBindingConstants.PROPERTY_TYPE))) {
327 HaywardBackyardHandler handler = (HaywardBackyardHandler) thing.getHandler();
328 if (handler != null) {
329 String systemID = properties.get(HaywardBindingConstants.PROPERTY_SYSTEM_ID);
330 if (systemID != null) {
331 return handler.getAlarmList(systemID);
339 private synchronized void initPolling(int initalDelay) {
340 pollTelemetryFuture = scheduler.scheduleWithFixedDelay(() -> {
342 if (commFailureCount >= 5) {
343 commFailureCount = 0;
344 clearPolling(pollTelemetryFuture);
345 clearPolling(pollAlarmsFuture);
349 if (!(getTelemetryData())) {
353 } catch (HaywardException e) {
354 logger.debug("Hayward Connection thing: Exception during poll: {}", e.getMessage());
355 } catch (InterruptedException e) {
358 }, initalDelay, config.telemetryPollTime, TimeUnit.SECONDS);
362 private synchronized void initAlarmPolling(int initalDelay) {
363 pollAlarmsFuture = scheduler.scheduleWithFixedDelay(() -> {
366 } catch (HaywardException e) {
367 logger.debug("Hayward Connection thing: Exception during poll: {}", e.getMessage());
369 }, initalDelay, config.alarmPollTime, TimeUnit.SECONDS);
372 private void clearPolling(@Nullable ScheduledFuture<?> pollJob) {
373 if (pollJob != null) {
374 pollJob.cancel(false);
379 Thing getThingForType(HaywardTypeToRequest type, int num) {
380 for (Thing thing : getThing().getThings()) {
381 Map<String, String> properties = thing.getProperties();
382 if (Integer.toString(num).equals(properties.get(HaywardBindingConstants.PROPERTY_SYSTEM_ID))) {
383 if (type.toString().equals(properties.get(HaywardBindingConstants.PROPERTY_TYPE))) {
391 public List<String> evaluateXPath(String xpathExp, String xmlResponse) {
392 List<String> values = new ArrayList<>();
394 InputSource inputXML = new InputSource(new StringReader(xmlResponse));
395 XPath xPath = XPathFactory.newInstance().newXPath();
396 NodeList nodes = (NodeList) xPath.evaluate(xpathExp, inputXML, XPathConstants.NODESET);
398 for (int i = 0; i < nodes.getLength(); i++) {
399 values.add(nodes.item(i).getNodeValue());
401 } catch (XPathExpressionException e) {
402 logger.warn("XPathExpression exception: {}", e.getMessage());
407 private Request sendRequestBuilder(String url, HttpMethod method) {
408 return this.httpClient.newRequest(url).agent("NextGenForIPhone/16565 CFNetwork/887 Darwin/17.0.0")
409 .method(method).header(HttpHeader.ACCEPT_LANGUAGE, "en-us").header(HttpHeader.ACCEPT, "*/*")
410 .header(HttpHeader.ACCEPT_ENCODING, "gzip, deflate").version(HttpVersion.HTTP_1_1)
411 .header(HttpHeader.CONNECTION, "keep-alive").header(HttpHeader.HOST, "www.haywardomnilogic.com:80")
412 .timeout(10, TimeUnit.SECONDS);
415 public synchronized String httpXmlResponse(String urlParameters) throws HaywardException, InterruptedException {
416 String urlParameterslength = Integer.toString(urlParameters.length());
417 String statusMessage;
420 ContentResponse httpResponse = sendRequestBuilder(config.endpointUrl, HttpMethod.POST)
421 .content(new StringContentProvider(urlParameters), "text/xml; charset=utf-8")
422 .header(HttpHeader.CONTENT_LENGTH, urlParameterslength).send();
424 int status = httpResponse.getStatus();
425 String xmlResponse = httpResponse.getContentAsString();
427 List<String> statusMessages = evaluateXPath("/Response/Parameters//Parameter[@name='StatusMessage']/text()",
429 if (!(statusMessages.isEmpty())) {
430 statusMessage = statusMessages.get(0);
432 statusMessage = httpResponse.getReason();
436 if (logger.isTraceEnabled()) {
437 logger.trace("Hayward Connection thing: {} Hayward http command: {}", getCallingMethod(),
439 logger.trace("Hayward Connection thing: {} Hayward http response: {} {}", getCallingMethod(),
440 statusMessage, xmlResponse);
444 if (logger.isDebugEnabled()) {
445 logger.debug("Hayward Connection thing: {} Hayward http command: {}", getCallingMethod(),
447 logger.debug("Hayward Connection thing: {} Hayward http response: {}", getCallingMethod(), status);
451 } catch (ExecutionException e) {
452 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
453 "Unable to resolve host. Check Hayward hostname and your internet connection. " + e);
455 } catch (TimeoutException e) {
456 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
457 "Connection Timeout. Check Hayward hostname and your internet connection. " + e);
462 private String getCallingMethod() {
463 StackTraceElement[] stacktrace = Thread.currentThread().getStackTrace();
464 StackTraceElement e = stacktrace[3];
465 return e.getMethodName();
468 public int convertCommand(Command command) {
469 if (command == OnOffType.ON) {