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.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.HaywardDynamicStateDescriptionProvider;
43 import org.openhab.binding.haywardomnilogic.internal.HaywardException;
44 import org.openhab.binding.haywardomnilogic.internal.HaywardThingHandler;
45 import org.openhab.binding.haywardomnilogic.internal.HaywardTypeToRequest;
46 import org.openhab.binding.haywardomnilogic.internal.config.HaywardConfig;
47 import org.openhab.binding.haywardomnilogic.internal.discovery.HaywardDiscoveryService;
48 import org.openhab.core.library.types.OnOffType;
49 import org.openhab.core.thing.Bridge;
50 import org.openhab.core.thing.Channel;
51 import org.openhab.core.thing.ChannelUID;
52 import org.openhab.core.thing.Thing;
53 import org.openhab.core.thing.ThingStatus;
54 import org.openhab.core.thing.ThingStatusDetail;
55 import org.openhab.core.thing.binding.BaseBridgeHandler;
56 import org.openhab.core.thing.binding.ThingHandlerService;
57 import org.openhab.core.types.Command;
58 import org.openhab.core.types.StateDescriptionFragment;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61 import org.w3c.dom.NodeList;
62 import org.xml.sax.InputSource;
65 * The {@link HaywardBridgeHandler} is responsible for handling commands, which are
66 * sent to one of the channels.
68 * @author Matt Myers - Initial contribution
72 public class HaywardBridgeHandler extends BaseBridgeHandler {
73 private final Logger logger = LoggerFactory.getLogger(HaywardBridgeHandler.class);
74 private final HaywardDynamicStateDescriptionProvider stateDescriptionProvider;
75 private final HttpClient httpClient;
76 private @Nullable ScheduledFuture<?> initializeFuture;
77 private @Nullable ScheduledFuture<?> pollTelemetryFuture;
78 private @Nullable ScheduledFuture<?> pollAlarmsFuture;
79 private int commFailureCount;
80 public HaywardConfig config = getConfig().as(HaywardConfig.class);
81 public HaywardAccount account = getConfig().as(HaywardAccount.class);
84 public Collection<Class<? extends ThingHandlerService>> getServices() {
85 return Collections.singleton(HaywardDiscoveryService.class);
88 public HaywardBridgeHandler(HaywardDynamicStateDescriptionProvider stateDescriptionProvider, Bridge bridge,
89 HttpClient httpClient) {
91 this.httpClient = httpClient;
92 this.stateDescriptionProvider = stateDescriptionProvider;
96 public void handleCommand(ChannelUID channelUID, Command command) {
100 public void dispose() {
101 clearPolling(initializeFuture);
102 clearPolling(pollTelemetryFuture);
103 clearPolling(pollAlarmsFuture);
104 logger.trace("Hayward polling cancelled");
109 public void initialize() {
110 initializeFuture = scheduler.schedule(this::scheduledInitialize, 1, TimeUnit.SECONDS);
114 public void scheduledInitialize() {
115 config = getConfigAs(HaywardConfig.class);
118 clearPolling(pollTelemetryFuture);
119 clearPolling(pollAlarmsFuture);
122 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
123 "Unable to Login to Hayward's server");
124 clearPolling(pollTelemetryFuture);
125 clearPolling(pollAlarmsFuture);
126 commFailureCount = 50;
131 if (!(getSiteList())) {
132 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
133 "Unable to getMSP from Hayward's server");
134 clearPolling(pollTelemetryFuture);
135 clearPolling(pollAlarmsFuture);
136 commFailureCount = 50;
141 if (!(mspConfigUnits())) {
142 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
143 "Unable to getMSPConfigUnits from Hayward's server");
144 clearPolling(pollTelemetryFuture);
145 clearPolling(pollAlarmsFuture);
146 commFailureCount = 50;
151 if (logger.isTraceEnabled()) {
152 if (!(getApiDef())) {
153 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
154 "Unable to getApiDef from Hayward's server");
155 clearPolling(pollTelemetryFuture);
156 clearPolling(pollAlarmsFuture);
157 commFailureCount = 50;
163 if (this.thing.getStatus() != ThingStatus.ONLINE) {
164 updateStatus(ThingStatus.ONLINE);
167 logger.debug("Succesfully opened connection to Hayward's server: {} Username:{}", config.endpointUrl,
171 logger.trace("Hayward Telemetry polling scheduled");
173 if (config.alarmPollTime > 0) {
176 } catch (HaywardException e) {
177 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR,
178 "scheduledInitialize exception: " + e.getMessage());
179 clearPolling(pollTelemetryFuture);
180 clearPolling(pollAlarmsFuture);
181 commFailureCount = 50;
184 } catch (InterruptedException e) {
189 public synchronized boolean login() throws HaywardException, InterruptedException {
193 // *****Login to Hayward server
194 String urlParameters = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Request>" + "<Name>Login</Name><Parameters>"
195 + "<Parameter name=\"UserName\" dataType=\"String\">" + config.username + "</Parameter>"
196 + "<Parameter name=\"Password\" dataType=\"String\">" + config.password + "</Parameter>"
197 + "</Parameters></Request>";
199 xmlResponse = httpXmlResponse(urlParameters);
201 if (xmlResponse.isEmpty()) {
202 logger.debug("Hayward Connection thing: Login XML response was null");
206 status = evaluateXPath("/Response/Parameters//Parameter[@name='Status']/text()", xmlResponse).get(0);
208 if (!("0".equals(status))) {
209 logger.debug("Hayward Connection thing: Login XML response: {}", xmlResponse);
213 account.token = evaluateXPath("/Response/Parameters//Parameter[@name='Token']/text()", xmlResponse).get(0);
214 account.userID = evaluateXPath("/Response/Parameters//Parameter[@name='UserID']/text()", xmlResponse).get(0);
218 public synchronized boolean getApiDef() throws HaywardException, InterruptedException {
221 // *****getApiDef from Hayward server
222 String urlParameters = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Request><Name>GetAPIDef</Name><Parameters>"
223 + "<Parameter name=\"Token\" dataType=\"String\">" + account.token + "</Parameter>"
224 + "<Parameter name=\"MspSystemID\" dataType=\"int\">" + account.mspSystemID + "</Parameter>;"
225 + "<Parameter name=\"Version\" dataType=\"string\">0.4</Parameter >\r\n"
226 + "<Parameter name=\"Language\" dataType=\"string\">en</Parameter >\r\n" + "</Parameters></Request>";
228 xmlResponse = httpXmlResponse(urlParameters);
230 if (xmlResponse.isEmpty()) {
231 logger.debug("Hayward Connection thing: getApiDef XML response was null");
237 public synchronized boolean getSiteList() throws HaywardException, InterruptedException {
242 String urlParameters = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Request><Name>GetSiteList</Name><Parameters>"
243 + "<Parameter name=\"Token\" dataType=\"String\">" + account.token
244 + "</Parameter><Parameter name=\"UserID\" dataType=\"String\">" + account.userID
245 + "</Parameter></Parameters></Request>";
247 xmlResponse = httpXmlResponse(urlParameters);
249 if (xmlResponse.isEmpty()) {
250 logger.debug("Hayward Connection thing: getSiteList XML response was null");
254 status = evaluateXPath("/Response/Parameters//Parameter[@name='Status']/text()", xmlResponse).get(0);
256 if (!("0".equals(status))) {
257 logger.debug("Hayward Connection thing: getSiteList XML response: {}", xmlResponse);
261 account.mspSystemID = evaluateXPath("/Response/Parameters/Parameter/Item//Property[@name='MspSystemID']/text()",
263 account.backyardName = evaluateXPath(
264 "/Response/Parameters/Parameter/Item//Property[@name='BackyardName']/text()", xmlResponse).get(0);
265 account.address = evaluateXPath("/Response/Parameters/Parameter/Item//Property[@name='Address']/text()",
270 public synchronized String getMspConfig() throws HaywardException, InterruptedException {
271 // *****getMspConfig from Hayward server
272 String urlParameters = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Request><Name>GetMspConfigFile</Name><Parameters>"
273 + "<Parameter name=\"Token\" dataType=\"String\">" + account.token + "</Parameter>"
274 + "<Parameter name=\"MspSystemID\" dataType=\"int\">" + account.mspSystemID
275 + "</Parameter><Parameter name=\"Version\" dataType=\"string\">0</Parameter>\r\n"
276 + "</Parameters></Request>";
278 String xmlResponse = httpXmlResponse(urlParameters);
280 if (xmlResponse.isEmpty()) {
281 logger.debug("Hayward Connection thing: getMSPConfig XML response was null");
285 if (evaluateXPath("//Backyard/Name/text()", xmlResponse).isEmpty()) {
286 logger.debug("Hayward Connection thing: getMSPConfig XML response: {}", xmlResponse);
292 public synchronized boolean mspConfigUnits() throws HaywardException, InterruptedException {
293 List<String> property1 = new ArrayList<>();
294 List<String> property2 = new ArrayList<>();
296 String xmlResponse = getMspConfig();
298 if (xmlResponse.contentEquals("Fail")) {
302 // Get Units (Standard, Metric)
303 property1 = evaluateXPath("//System/Units/text()", xmlResponse);
304 account.units = property1.get(0);
306 // Get Variable Speed Pump Units (percent, RPM)
307 property2 = evaluateXPath("//System/Msp-Vsp-Speed-Format/text()", xmlResponse);
308 account.vspSpeedFormat = property2.get(0);
313 public synchronized boolean getTelemetryData() throws HaywardException, InterruptedException {
314 // *****getTelemetry from Hayward server
315 String urlParameters = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Request><Name>GetTelemetryData</Name><Parameters>"
316 + "<Parameter name=\"Token\" dataType=\"String\">" + account.token + "</Parameter>"
317 + "<Parameter name=\"MspSystemID\" dataType=\"int\">" + account.mspSystemID
318 + "</Parameter></Parameters></Request>";
320 String xmlResponse = httpXmlResponse(urlParameters);
322 if (xmlResponse.isEmpty()) {
323 logger.debug("Hayward Connection thing: getTelemetry XML response was null");
327 if (!evaluateXPath("/Response/Parameters//Parameter[@name='StatusMessage']/text()", xmlResponse).isEmpty()) {
328 logger.debug("Hayward Connection thing: getTelemetry XML response: {}", xmlResponse);
332 for (Thing thing : getThing().getThings()) {
333 if (thing.getHandler() instanceof HaywardThingHandler) {
334 HaywardThingHandler handler = (HaywardThingHandler) thing.getHandler();
335 if (handler != null) {
336 handler.getTelemetry(xmlResponse);
343 public synchronized boolean getAlarmList() throws HaywardException {
344 for (Thing thing : getThing().getThings()) {
345 Map<String, String> properties = thing.getProperties();
346 if ("BACKYARD".equals(properties.get(HaywardBindingConstants.PROPERTY_TYPE))) {
347 HaywardBackyardHandler handler = (HaywardBackyardHandler) thing.getHandler();
348 if (handler != null) {
349 String systemID = properties.get(HaywardBindingConstants.PROPERTY_SYSTEM_ID);
350 if (systemID != null) {
351 return handler.getAlarmList(systemID);
359 private synchronized void initPolling(int initalDelay) {
360 pollTelemetryFuture = scheduler.scheduleWithFixedDelay(() -> {
362 if (commFailureCount >= 5) {
363 commFailureCount = 0;
364 clearPolling(pollTelemetryFuture);
365 clearPolling(pollAlarmsFuture);
369 if (!(getTelemetryData())) {
373 updateStatus(ThingStatus.ONLINE);
374 } catch (HaywardException e) {
375 logger.debug("Hayward Connection thing: Exception during poll: {}", e.getMessage());
376 } catch (InterruptedException e) {
379 }, initalDelay, config.telemetryPollTime, TimeUnit.SECONDS);
383 private synchronized void initAlarmPolling(int initalDelay) {
384 pollAlarmsFuture = scheduler.scheduleWithFixedDelay(() -> {
387 } catch (HaywardException e) {
388 logger.debug("Hayward Connection thing: Exception during poll: {}", e.getMessage());
390 }, initalDelay, config.alarmPollTime, TimeUnit.SECONDS);
393 private void clearPolling(@Nullable ScheduledFuture<?> pollJob) {
394 if (pollJob != null) {
395 pollJob.cancel(false);
400 Thing getThingForType(HaywardTypeToRequest type, int num) {
401 for (Thing thing : getThing().getThings()) {
402 Map<String, String> properties = thing.getProperties();
403 if (Integer.toString(num).equals(properties.get(HaywardBindingConstants.PROPERTY_SYSTEM_ID))) {
404 if (type.toString().equals(properties.get(HaywardBindingConstants.PROPERTY_TYPE))) {
412 public List<String> evaluateXPath(String xpathExp, String xmlResponse) {
413 List<String> values = new ArrayList<>();
415 InputSource inputXML = new InputSource(new StringReader(xmlResponse));
416 XPath xPath = XPathFactory.newInstance().newXPath();
417 NodeList nodes = (NodeList) xPath.evaluate(xpathExp, inputXML, XPathConstants.NODESET);
419 for (int i = 0; i < nodes.getLength(); i++) {
420 values.add(nodes.item(i).getNodeValue());
422 } catch (XPathExpressionException e) {
423 logger.warn("XPathExpression exception: {}", e.getMessage());
428 private Request sendRequestBuilder(String url, HttpMethod method) {
429 return this.httpClient.newRequest(url).agent("NextGenForIPhone/16565 CFNetwork/887 Darwin/17.0.0")
430 .method(method).header(HttpHeader.ACCEPT_LANGUAGE, "en-us").header(HttpHeader.ACCEPT, "*/*")
431 .header(HttpHeader.ACCEPT_ENCODING, "gzip, deflate").version(HttpVersion.HTTP_1_1)
432 .header(HttpHeader.CONNECTION, "keep-alive").header(HttpHeader.HOST, "www.haywardomnilogic.com:80")
433 .timeout(10, TimeUnit.SECONDS);
436 public synchronized String httpXmlResponse(String urlParameters) throws HaywardException, InterruptedException {
437 String urlParameterslength = Integer.toString(urlParameters.length());
438 String statusMessage;
440 if (logger.isTraceEnabled()) {
441 logger.trace("Hayward Connection thing: {} Hayward http command: {}", getCallingMethod(), urlParameters);
442 } else if (logger.isDebugEnabled()) {
443 logger.debug("Hayward Connection thing: {}", getCallingMethod());
446 for (int retry = 0; retry <= 2; retry++) {
448 ContentResponse httpResponse = sendRequestBuilder(config.endpointUrl, HttpMethod.POST)
449 .content(new StringContentProvider(urlParameters), "text/xml; charset=utf-8")
450 .header(HttpHeader.CONTENT_LENGTH, urlParameterslength).send();
452 int status = httpResponse.getStatus();
453 String xmlResponse = httpResponse.getContentAsString();
456 List<String> statusMessages = evaluateXPath(
457 "/Response/Parameters//Parameter[@name='StatusMessage']/text()", xmlResponse);
458 if (!(statusMessages.isEmpty())) {
459 statusMessage = statusMessages.get(0);
461 statusMessage = httpResponse.getReason();
464 if (logger.isTraceEnabled()) {
465 logger.trace("Hayward Connection thing: {} Hayward http response: {} {}", getCallingMethod(),
466 statusMessage, xmlResponse);
470 if (logger.isDebugEnabled()) {
471 logger.debug("Hayward Connection thing: {} Hayward http response: {} {}", getCallingMethod(),
472 status, xmlResponse);
476 } catch (ExecutionException e) {
477 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
478 "Unable to resolve host. Check Hayward hostname and your internet connection. "
481 } catch (TimeoutException e) {
483 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
484 "Connection Timeout. Check Hayward hostname and your internet connection. "
488 logger.warn("Hayward Connection thing Timeout: {} Try: {} ", getCallingMethod(), retry + 1);
495 private String getCallingMethod() {
496 StackTraceElement[] stacktrace = Thread.currentThread().getStackTrace();
497 StackTraceElement e = stacktrace[3];
498 return e.getMethodName();
501 void updateChannelStateDescriptionFragment(Channel channel, StateDescriptionFragment descriptionFragment) {
502 ChannelUID channelId = channel.getUID();
503 stateDescriptionProvider.setStateDescriptionFragment(channelId, descriptionFragment);
506 public int convertCommand(Command command) {
507 if (command == OnOffType.ON) {