2 * Copyright (c) 2010-2022 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()) {
205 status = evaluateXPath("/Response/Parameters//Parameter[@name='Status']/text()", xmlResponse).get(0);
207 if (!("0".equals(status))) {
208 logger.debug("Hayward Connection thing: Login XML response: {}", xmlResponse);
212 account.token = evaluateXPath("/Response/Parameters//Parameter[@name='Token']/text()", xmlResponse).get(0);
213 account.userID = evaluateXPath("/Response/Parameters//Parameter[@name='UserID']/text()", xmlResponse).get(0);
217 public synchronized boolean getApiDef() throws HaywardException, InterruptedException {
220 // *****getApiDef from Hayward server
221 String urlParameters = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Request><Name>GetAPIDef</Name><Parameters>"
222 + "<Parameter name=\"Token\" dataType=\"String\">" + account.token + "</Parameter>"
223 + "<Parameter name=\"MspSystemID\" dataType=\"int\">" + account.mspSystemID + "</Parameter>;"
224 + "<Parameter name=\"Version\" dataType=\"string\">0.4</Parameter >\r\n"
225 + "<Parameter name=\"Language\" dataType=\"string\">en</Parameter >\r\n" + "</Parameters></Request>";
227 xmlResponse = httpXmlResponse(urlParameters);
229 if (xmlResponse.isEmpty()) {
230 logger.debug("Hayward Connection thing: Login XML response was null");
236 public synchronized boolean getSiteList() throws HaywardException, InterruptedException {
241 String urlParameters = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Request><Name>GetSiteList</Name><Parameters>"
242 + "<Parameter name=\"Token\" dataType=\"String\">" + account.token
243 + "</Parameter><Parameter name=\"UserID\" dataType=\"String\">" + account.userID
244 + "</Parameter></Parameters></Request>";
246 xmlResponse = httpXmlResponse(urlParameters);
248 if (xmlResponse.isEmpty()) {
249 logger.debug("Hayward Connection thing: getSiteList XML response was null");
253 status = evaluateXPath("/Response/Parameters//Parameter[@name='Status']/text()", xmlResponse).get(0);
255 if (!("0".equals(status))) {
256 logger.debug("Hayward Connection thing: getSiteList XML response: {}", xmlResponse);
260 account.mspSystemID = evaluateXPath("/Response/Parameters/Parameter/Item//Property[@name='MspSystemID']/text()",
262 account.backyardName = evaluateXPath(
263 "/Response/Parameters/Parameter/Item//Property[@name='BackyardName']/text()", xmlResponse).get(0);
264 account.address = evaluateXPath("/Response/Parameters/Parameter/Item//Property[@name='Address']/text()",
269 public synchronized String getMspConfig() throws HaywardException, InterruptedException {
270 // *****getMspConfig from Hayward server
271 String urlParameters = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Request><Name>GetMspConfigFile</Name><Parameters>"
272 + "<Parameter name=\"Token\" dataType=\"String\">" + account.token + "</Parameter>"
273 + "<Parameter name=\"MspSystemID\" dataType=\"int\">" + account.mspSystemID
274 + "</Parameter><Parameter name=\"Version\" dataType=\"string\">0</Parameter>\r\n"
275 + "</Parameters></Request>";
277 String xmlResponse = httpXmlResponse(urlParameters);
279 if (xmlResponse.isEmpty()) {
280 logger.debug("Hayward Connection thing: requestConfig XML response was null");
284 if (evaluateXPath("//Backyard/Name/text()", xmlResponse).isEmpty()) {
285 logger.debug("Hayward Connection thing: requestConfiguration XML response: {}", xmlResponse);
291 public synchronized boolean mspConfigUnits() throws HaywardException, InterruptedException {
292 List<String> property1 = new ArrayList<>();
293 List<String> property2 = new ArrayList<>();
295 String xmlResponse = getMspConfig();
297 // Get Units (Standard, Metric)
298 property1 = evaluateXPath("//System/Units/text()", xmlResponse);
299 account.units = property1.get(0);
301 // Get Variable Speed Pump Units (percent, RPM)
302 property2 = evaluateXPath("//System/Msp-Vsp-Speed-Format/text()", xmlResponse);
303 account.vspSpeedFormat = property2.get(0);
308 public synchronized boolean getTelemetryData() throws HaywardException, InterruptedException {
309 // *****getTelemetry from Hayward server
310 String urlParameters = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Request><Name>GetTelemetryData</Name><Parameters>"
311 + "<Parameter name=\"Token\" dataType=\"String\">" + account.token + "</Parameter>"
312 + "<Parameter name=\"MspSystemID\" dataType=\"int\">" + account.mspSystemID
313 + "</Parameter></Parameters></Request>";
315 String xmlResponse = httpXmlResponse(urlParameters);
317 if (xmlResponse.isEmpty()) {
318 logger.debug("Hayward Connection thing: getTelemetry XML response was null");
322 if (!evaluateXPath("/Response/Parameters//Parameter[@name='StatusMessage']/text()", xmlResponse).isEmpty()) {
323 logger.debug("Hayward Connection thing: getTelemetry XML response: {}", xmlResponse);
327 for (Thing thing : getThing().getThings()) {
328 if (thing.getHandler() instanceof HaywardThingHandler) {
329 HaywardThingHandler handler = (HaywardThingHandler) thing.getHandler();
330 if (handler != null) {
331 handler.getTelemetry(xmlResponse);
338 public synchronized boolean getAlarmList() throws HaywardException {
339 for (Thing thing : getThing().getThings()) {
340 Map<String, String> properties = thing.getProperties();
341 if ("BACKYARD".equals(properties.get(HaywardBindingConstants.PROPERTY_TYPE))) {
342 HaywardBackyardHandler handler = (HaywardBackyardHandler) thing.getHandler();
343 if (handler != null) {
344 String systemID = properties.get(HaywardBindingConstants.PROPERTY_SYSTEM_ID);
345 if (systemID != null) {
346 return handler.getAlarmList(systemID);
354 private synchronized void initPolling(int initalDelay) {
355 pollTelemetryFuture = scheduler.scheduleWithFixedDelay(() -> {
357 if (commFailureCount >= 5) {
358 commFailureCount = 0;
359 clearPolling(pollTelemetryFuture);
360 clearPolling(pollAlarmsFuture);
364 if (!(getTelemetryData())) {
368 updateStatus(ThingStatus.ONLINE);
369 } catch (HaywardException e) {
370 logger.debug("Hayward Connection thing: Exception during poll: {}", e.getMessage());
371 } catch (InterruptedException e) {
374 }, initalDelay, config.telemetryPollTime, TimeUnit.SECONDS);
378 private synchronized void initAlarmPolling(int initalDelay) {
379 pollAlarmsFuture = scheduler.scheduleWithFixedDelay(() -> {
382 } catch (HaywardException e) {
383 logger.debug("Hayward Connection thing: Exception during poll: {}", e.getMessage());
385 }, initalDelay, config.alarmPollTime, TimeUnit.SECONDS);
388 private void clearPolling(@Nullable ScheduledFuture<?> pollJob) {
389 if (pollJob != null) {
390 pollJob.cancel(false);
395 Thing getThingForType(HaywardTypeToRequest type, int num) {
396 for (Thing thing : getThing().getThings()) {
397 Map<String, String> properties = thing.getProperties();
398 if (Integer.toString(num).equals(properties.get(HaywardBindingConstants.PROPERTY_SYSTEM_ID))) {
399 if (type.toString().equals(properties.get(HaywardBindingConstants.PROPERTY_TYPE))) {
407 public List<String> evaluateXPath(String xpathExp, String xmlResponse) {
408 List<String> values = new ArrayList<>();
410 InputSource inputXML = new InputSource(new StringReader(xmlResponse));
411 XPath xPath = XPathFactory.newInstance().newXPath();
412 NodeList nodes = (NodeList) xPath.evaluate(xpathExp, inputXML, XPathConstants.NODESET);
414 for (int i = 0; i < nodes.getLength(); i++) {
415 values.add(nodes.item(i).getNodeValue());
417 } catch (XPathExpressionException e) {
418 logger.warn("XPathExpression exception: {}", e.getMessage());
423 private Request sendRequestBuilder(String url, HttpMethod method) {
424 return this.httpClient.newRequest(url).agent("NextGenForIPhone/16565 CFNetwork/887 Darwin/17.0.0")
425 .method(method).header(HttpHeader.ACCEPT_LANGUAGE, "en-us").header(HttpHeader.ACCEPT, "*/*")
426 .header(HttpHeader.ACCEPT_ENCODING, "gzip, deflate").version(HttpVersion.HTTP_1_1)
427 .header(HttpHeader.CONNECTION, "keep-alive").header(HttpHeader.HOST, "www.haywardomnilogic.com:80")
428 .timeout(10, TimeUnit.SECONDS);
431 public synchronized String httpXmlResponse(String urlParameters) throws HaywardException, InterruptedException {
432 String urlParameterslength = Integer.toString(urlParameters.length());
433 String statusMessage;
436 ContentResponse httpResponse = sendRequestBuilder(config.endpointUrl, HttpMethod.POST)
437 .content(new StringContentProvider(urlParameters), "text/xml; charset=utf-8")
438 .header(HttpHeader.CONTENT_LENGTH, urlParameterslength).send();
440 int status = httpResponse.getStatus();
441 String xmlResponse = httpResponse.getContentAsString();
444 List<String> statusMessages = evaluateXPath(
445 "/Response/Parameters//Parameter[@name='StatusMessage']/text()", xmlResponse);
446 if (!(statusMessages.isEmpty())) {
447 statusMessage = statusMessages.get(0);
449 statusMessage = httpResponse.getReason();
452 if (logger.isTraceEnabled()) {
453 logger.trace("Hayward Connection thing: {} Hayward http command: {}", getCallingMethod(),
455 logger.trace("Hayward Connection thing: {} Hayward http response: {} {}", getCallingMethod(),
456 statusMessage, xmlResponse);
460 if (logger.isDebugEnabled()) {
461 logger.debug("Hayward Connection thing: {} Hayward http command: {}", getCallingMethod(),
463 logger.debug("Hayward Connection thing: {} Hayward http response: {} {}", getCallingMethod(),
464 status, xmlResponse);
468 } catch (ExecutionException e) {
469 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
470 "Unable to resolve host. Check Hayward hostname and your internet connection. " + e);
472 } catch (TimeoutException e) {
473 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
474 "Connection Timeout. Check Hayward hostname and your internet connection. " + e);
479 private String getCallingMethod() {
480 StackTraceElement[] stacktrace = Thread.currentThread().getStackTrace();
481 StackTraceElement e = stacktrace[3];
482 return e.getMethodName();
485 void updateChannelStateDescriptionFragment(Channel channel, StateDescriptionFragment descriptionFragment) {
486 ChannelUID channelId = channel.getUID();
487 stateDescriptionProvider.setStateDescriptionFragment(channelId, descriptionFragment);
490 public int convertCommand(Command command) {
491 if (command == OnOffType.ON) {