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.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 Set.of(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 = """
195 <?xml version="1.0" encoding="utf-8"?><Request>\
196 <Name>Login</Name><Parameters>\
197 <Parameter name="UserName" dataType="String">\
198 """ + config.username + "</Parameter>" + "<Parameter name=\"Password\" dataType=\"String\">"
199 + config.password + "</Parameter>" + "</Parameters></Request>";
201 xmlResponse = httpXmlResponse(urlParameters);
203 if (xmlResponse.isEmpty()) {
204 logger.debug("Hayward Connection thing: Login XML response was null");
208 status = evaluateXPath("/Response/Parameters//Parameter[@name='Status']/text()", xmlResponse).get(0);
210 if (!("0".equals(status))) {
211 logger.debug("Hayward Connection thing: Login XML response: {}", xmlResponse);
215 account.token = evaluateXPath("/Response/Parameters//Parameter[@name='Token']/text()", xmlResponse).get(0);
216 account.userID = evaluateXPath("/Response/Parameters//Parameter[@name='UserID']/text()", xmlResponse).get(0);
220 public synchronized boolean getApiDef() throws HaywardException, InterruptedException {
223 // *****getApiDef from Hayward server
224 String urlParameters = """
225 <?xml version="1.0" encoding="utf-8"?><Request><Name>GetAPIDef</Name><Parameters>\
226 <Parameter name="Token" dataType="String">\
227 """ + account.token + "</Parameter>" + "<Parameter name=\"MspSystemID\" dataType=\"int\">"
228 + account.mspSystemID + "</Parameter>;"
229 + "<Parameter name=\"Version\" dataType=\"string\">0.4</Parameter >\r\n"
230 + "<Parameter name=\"Language\" dataType=\"string\">en</Parameter >\r\n" + "</Parameters></Request>";
232 xmlResponse = httpXmlResponse(urlParameters);
234 if (xmlResponse.isEmpty()) {
235 logger.debug("Hayward Connection thing: getApiDef XML response was null");
241 public synchronized boolean getSiteList() throws HaywardException, InterruptedException {
246 String urlParameters = """
247 <?xml version="1.0" encoding="utf-8"?><Request><Name>GetSiteList</Name><Parameters>\
248 <Parameter name="Token" dataType="String">\
249 """ + account.token + "</Parameter><Parameter name=\"UserID\" dataType=\"String\">" + account.userID
250 + "</Parameter></Parameters></Request>";
252 xmlResponse = httpXmlResponse(urlParameters);
254 if (xmlResponse.isEmpty()) {
255 logger.debug("Hayward Connection thing: getSiteList XML response was null");
259 status = evaluateXPath("/Response/Parameters//Parameter[@name='Status']/text()", xmlResponse).get(0);
261 if (!("0".equals(status))) {
262 logger.debug("Hayward Connection thing: getSiteList XML response: {}", xmlResponse);
266 account.mspSystemID = evaluateXPath("/Response/Parameters/Parameter/Item//Property[@name='MspSystemID']/text()",
268 account.backyardName = evaluateXPath(
269 "/Response/Parameters/Parameter/Item//Property[@name='BackyardName']/text()", xmlResponse).get(0);
270 account.address = evaluateXPath("/Response/Parameters/Parameter/Item//Property[@name='Address']/text()",
275 public synchronized String getMspConfig() throws HaywardException, InterruptedException {
276 // *****getMspConfig from Hayward server
277 String urlParameters = """
278 <?xml version="1.0" encoding="utf-8"?><Request><Name>GetMspConfigFile</Name><Parameters>\
279 <Parameter name="Token" dataType="String">\
280 """ + account.token + "</Parameter>" + "<Parameter name=\"MspSystemID\" dataType=\"int\">"
281 + account.mspSystemID + "</Parameter><Parameter name=\"Version\" dataType=\"string\">0</Parameter>\r\n"
282 + "</Parameters></Request>";
284 String xmlResponse = httpXmlResponse(urlParameters);
286 if (xmlResponse.isEmpty()) {
287 logger.debug("Hayward Connection thing: getMSPConfig XML response was null");
291 if (evaluateXPath("//Backyard/Name/text()", xmlResponse).isEmpty()) {
292 logger.debug("Hayward Connection thing: getMSPConfig XML response: {}", xmlResponse);
298 public synchronized boolean mspConfigUnits() throws HaywardException, InterruptedException {
299 List<String> property1 = new ArrayList<>();
300 List<String> property2 = new ArrayList<>();
302 String xmlResponse = getMspConfig();
304 if (xmlResponse.contentEquals("Fail")) {
308 // Get Units (Standard, Metric)
309 property1 = evaluateXPath("//System/Units/text()", xmlResponse);
310 account.units = property1.get(0);
312 // Get Variable Speed Pump Units (percent, RPM)
313 property2 = evaluateXPath("//System/Msp-Vsp-Speed-Format/text()", xmlResponse);
314 account.vspSpeedFormat = property2.get(0);
319 public synchronized boolean getTelemetryData() throws HaywardException, InterruptedException {
320 // *****getTelemetry from Hayward server
321 String urlParameters = """
322 <?xml version="1.0" encoding="utf-8"?><Request><Name>GetTelemetryData</Name><Parameters>\
323 <Parameter name="Token" dataType="String">\
324 """ + account.token + "</Parameter>" + "<Parameter name=\"MspSystemID\" dataType=\"int\">"
325 + account.mspSystemID + "</Parameter></Parameters></Request>";
327 String xmlResponse = httpXmlResponse(urlParameters);
329 if (xmlResponse.isEmpty()) {
330 logger.debug("Hayward Connection thing: getTelemetry XML response was null");
334 if (!evaluateXPath("/Response/Parameters//Parameter[@name='StatusMessage']/text()", xmlResponse).isEmpty()) {
335 logger.debug("Hayward Connection thing: getTelemetry XML response: {}", xmlResponse);
339 for (Thing thing : getThing().getThings()) {
340 if (thing.getHandler() instanceof HaywardThingHandler) {
341 HaywardThingHandler handler = (HaywardThingHandler) thing.getHandler();
342 if (handler != null) {
343 handler.getTelemetry(xmlResponse);
350 public synchronized boolean getAlarmList() throws HaywardException {
351 for (Thing thing : getThing().getThings()) {
352 Map<String, String> properties = thing.getProperties();
353 if ("BACKYARD".equals(properties.get(HaywardBindingConstants.PROPERTY_TYPE))) {
354 HaywardBackyardHandler handler = (HaywardBackyardHandler) thing.getHandler();
355 if (handler != null) {
356 String systemID = properties.get(HaywardBindingConstants.PROPERTY_SYSTEM_ID);
357 if (systemID != null) {
358 return handler.getAlarmList(systemID);
366 private synchronized void initPolling(int initalDelay) {
367 pollTelemetryFuture = scheduler.scheduleWithFixedDelay(() -> {
369 if (commFailureCount >= 5) {
370 commFailureCount = 0;
371 clearPolling(pollTelemetryFuture);
372 clearPolling(pollAlarmsFuture);
376 if (!(getTelemetryData())) {
380 updateStatus(ThingStatus.ONLINE);
381 } catch (HaywardException e) {
382 logger.debug("Hayward Connection thing: Exception during poll: {}", e.getMessage());
383 } catch (InterruptedException e) {
386 }, initalDelay, config.telemetryPollTime, TimeUnit.SECONDS);
390 private synchronized void initAlarmPolling(int initalDelay) {
391 pollAlarmsFuture = scheduler.scheduleWithFixedDelay(() -> {
394 } catch (HaywardException e) {
395 logger.debug("Hayward Connection thing: Exception during poll: {}", e.getMessage());
397 }, initalDelay, config.alarmPollTime, TimeUnit.SECONDS);
400 private void clearPolling(@Nullable ScheduledFuture<?> pollJob) {
401 if (pollJob != null) {
402 pollJob.cancel(false);
407 Thing getThingForType(HaywardTypeToRequest type, int num) {
408 for (Thing thing : getThing().getThings()) {
409 Map<String, String> properties = thing.getProperties();
410 if (Integer.toString(num).equals(properties.get(HaywardBindingConstants.PROPERTY_SYSTEM_ID))) {
411 if (type.toString().equals(properties.get(HaywardBindingConstants.PROPERTY_TYPE))) {
419 public List<String> evaluateXPath(String xpathExp, String xmlResponse) {
420 List<String> values = new ArrayList<>();
422 InputSource inputXML = new InputSource(new StringReader(xmlResponse));
423 XPath xPath = XPathFactory.newInstance().newXPath();
424 NodeList nodes = (NodeList) xPath.evaluate(xpathExp, inputXML, XPathConstants.NODESET);
426 for (int i = 0; i < nodes.getLength(); i++) {
427 values.add(nodes.item(i).getNodeValue());
429 } catch (XPathExpressionException e) {
430 logger.warn("XPathExpression exception: {}", e.getMessage());
435 private Request sendRequestBuilder(String url, HttpMethod method) {
436 return this.httpClient.newRequest(url).agent("NextGenForIPhone/16565 CFNetwork/887 Darwin/17.0.0")
437 .method(method).header(HttpHeader.ACCEPT_LANGUAGE, "en-us").header(HttpHeader.ACCEPT, "*/*")
438 .header(HttpHeader.ACCEPT_ENCODING, "gzip, deflate").version(HttpVersion.HTTP_1_1)
439 .header(HttpHeader.CONNECTION, "keep-alive").header(HttpHeader.HOST, "www.haywardomnilogic.com:80")
440 .timeout(10, TimeUnit.SECONDS);
443 public synchronized String httpXmlResponse(String urlParameters) throws HaywardException, InterruptedException {
444 String urlParameterslength = Integer.toString(urlParameters.length());
445 String statusMessage;
447 if (logger.isTraceEnabled()) {
448 logger.trace("Hayward Connection thing: {} Hayward http command: {}", getCallingMethod(), urlParameters);
449 } else if (logger.isDebugEnabled()) {
450 logger.debug("Hayward Connection thing: {}", getCallingMethod());
453 for (int retry = 0; retry <= 2; retry++) {
455 ContentResponse httpResponse = sendRequestBuilder(config.endpointUrl, HttpMethod.POST)
456 .content(new StringContentProvider(urlParameters), "text/xml; charset=utf-8")
457 .header(HttpHeader.CONTENT_LENGTH, urlParameterslength).send();
459 int status = httpResponse.getStatus();
460 String xmlResponse = httpResponse.getContentAsString();
463 List<String> statusMessages = evaluateXPath(
464 "/Response/Parameters//Parameter[@name='StatusMessage']/text()", xmlResponse);
465 if (!(statusMessages.isEmpty())) {
466 statusMessage = statusMessages.get(0);
468 statusMessage = httpResponse.getReason();
471 if (logger.isTraceEnabled()) {
472 logger.trace("Hayward Connection thing: {} Hayward http response: {} {}", getCallingMethod(),
473 statusMessage, xmlResponse);
477 if (logger.isDebugEnabled()) {
478 logger.debug("Hayward Connection thing: {} Hayward http response: {} {}", getCallingMethod(),
479 status, xmlResponse);
483 } catch (ExecutionException e) {
484 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
485 "Unable to resolve host. Check Hayward hostname and your internet connection. "
488 } catch (TimeoutException e) {
490 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
491 "Connection Timeout. Check Hayward hostname and your internet connection. "
495 logger.warn("Hayward Connection thing Timeout: {} Try: {} ", getCallingMethod(), retry + 1);
502 private String getCallingMethod() {
503 StackTraceElement[] stacktrace = Thread.currentThread().getStackTrace();
504 StackTraceElement e = stacktrace[3];
505 return e.getMethodName();
508 void updateChannelStateDescriptionFragment(Channel channel, StateDescriptionFragment descriptionFragment) {
509 ChannelUID channelId = channel.getUID();
510 stateDescriptionProvider.setStateDescriptionFragment(channelId, descriptionFragment);
513 public int convertCommand(Command command) {
514 if (command == OnOffType.ON) {