]> git.basschouten.com Git - openhab-addons.git/blob
51b7097a0ccba44bd377d4efefef160f85f0f7e4
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.haywardomnilogic.internal.handler;
14
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;
20 import java.util.Map;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24 import java.util.concurrent.TimeoutException;
25
26 import javax.xml.xpath.XPath;
27 import javax.xml.xpath.XPathConstants;
28 import javax.xml.xpath.XPathExpressionException;
29 import javax.xml.xpath.XPathFactory;
30
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;
63
64 /**
65  * The {@link HaywardBridgeHandler} is responsible for handling commands, which are
66  * sent to one of the channels.
67  *
68  * @author Matt Myers - Initial contribution
69  */
70
71 @NonNullByDefault
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);
82
83     @Override
84     public Collection<Class<? extends ThingHandlerService>> getServices() {
85         return Collections.singleton(HaywardDiscoveryService.class);
86     }
87
88     public HaywardBridgeHandler(HaywardDynamicStateDescriptionProvider stateDescriptionProvider, Bridge bridge,
89             HttpClient httpClient) {
90         super(bridge);
91         this.httpClient = httpClient;
92         this.stateDescriptionProvider = stateDescriptionProvider;
93     }
94
95     @Override
96     public void handleCommand(ChannelUID channelUID, Command command) {
97     }
98
99     @Override
100     public void dispose() {
101         clearPolling(initializeFuture);
102         clearPolling(pollTelemetryFuture);
103         clearPolling(pollAlarmsFuture);
104         logger.trace("Hayward polling cancelled");
105         super.dispose();
106     }
107
108     @Override
109     public void initialize() {
110         initializeFuture = scheduler.schedule(this::scheduledInitialize, 1, TimeUnit.SECONDS);
111         return;
112     }
113
114     public void scheduledInitialize() {
115         config = getConfigAs(HaywardConfig.class);
116
117         try {
118             clearPolling(pollTelemetryFuture);
119             clearPolling(pollAlarmsFuture);
120
121             if (!(login())) {
122                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
123                         "Unable to Login to Hayward's server");
124                 clearPolling(pollTelemetryFuture);
125                 clearPolling(pollAlarmsFuture);
126                 commFailureCount = 50;
127                 initPolling(60);
128                 return;
129             }
130
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;
137                 initPolling(60);
138                 return;
139             }
140
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;
147                 initPolling(60);
148                 return;
149             }
150
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;
158                     initPolling(60);
159                     return;
160                 }
161             }
162
163             if (this.thing.getStatus() != ThingStatus.ONLINE) {
164                 updateStatus(ThingStatus.ONLINE);
165             }
166
167             logger.debug("Succesfully opened connection to Hayward's server: {} Username:{}", config.endpointUrl,
168                     config.username);
169
170             initPolling(0);
171             logger.trace("Hayward Telemetry polling scheduled");
172
173             if (config.alarmPollTime > 0) {
174                 initAlarmPolling(1);
175             }
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;
182             initPolling(60);
183             return;
184         } catch (InterruptedException e) {
185             return;
186         }
187     }
188
189     public synchronized boolean login() throws HaywardException, InterruptedException {
190         String xmlResponse;
191         String status;
192
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>";
198
199         xmlResponse = httpXmlResponse(urlParameters);
200
201         if (xmlResponse.isEmpty()) {
202             logger.debug("Hayward Connection thing: Login XML response was null");
203             return false;
204         }
205
206         status = evaluateXPath("/Response/Parameters//Parameter[@name='Status']/text()", xmlResponse).get(0);
207
208         if (!("0".equals(status))) {
209             logger.debug("Hayward Connection thing: Login XML response: {}", xmlResponse);
210             return false;
211         }
212
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);
215         return true;
216     }
217
218     public synchronized boolean getApiDef() throws HaywardException, InterruptedException {
219         String xmlResponse;
220
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>";
227
228         xmlResponse = httpXmlResponse(urlParameters);
229
230         if (xmlResponse.isEmpty()) {
231             logger.debug("Hayward Connection thing: getApiDef XML response was null");
232             return false;
233         }
234         return true;
235     }
236
237     public synchronized boolean getSiteList() throws HaywardException, InterruptedException {
238         String xmlResponse;
239         String status;
240
241         // *****Get MSP
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>";
246
247         xmlResponse = httpXmlResponse(urlParameters);
248
249         if (xmlResponse.isEmpty()) {
250             logger.debug("Hayward Connection thing: getSiteList XML response was null");
251             return false;
252         }
253
254         status = evaluateXPath("/Response/Parameters//Parameter[@name='Status']/text()", xmlResponse).get(0);
255
256         if (!("0".equals(status))) {
257             logger.debug("Hayward Connection thing: getSiteList XML response: {}", xmlResponse);
258             return false;
259         }
260
261         account.mspSystemID = evaluateXPath("/Response/Parameters/Parameter/Item//Property[@name='MspSystemID']/text()",
262                 xmlResponse).get(0);
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()",
266                 xmlResponse).get(0);
267         return true;
268     }
269
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>";
277
278         String xmlResponse = httpXmlResponse(urlParameters);
279
280         if (xmlResponse.isEmpty()) {
281             logger.debug("Hayward Connection thing: getMSPConfig XML response was null");
282             return "Fail";
283         }
284
285         if (evaluateXPath("//Backyard/Name/text()", xmlResponse).isEmpty()) {
286             logger.debug("Hayward Connection thing: getMSPConfig XML response: {}", xmlResponse);
287             return "Fail";
288         }
289         return xmlResponse;
290     }
291
292     public synchronized boolean mspConfigUnits() throws HaywardException, InterruptedException {
293         List<String> property1 = new ArrayList<>();
294         List<String> property2 = new ArrayList<>();
295
296         String xmlResponse = getMspConfig();
297
298         if (xmlResponse.contentEquals("Fail")) {
299             return false;
300         }
301
302         // Get Units (Standard, Metric)
303         property1 = evaluateXPath("//System/Units/text()", xmlResponse);
304         account.units = property1.get(0);
305
306         // Get Variable Speed Pump Units (percent, RPM)
307         property2 = evaluateXPath("//System/Msp-Vsp-Speed-Format/text()", xmlResponse);
308         account.vspSpeedFormat = property2.get(0);
309
310         return true;
311     }
312
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>";
319
320         String xmlResponse = httpXmlResponse(urlParameters);
321
322         if (xmlResponse.isEmpty()) {
323             logger.debug("Hayward Connection thing: getTelemetry XML response was null");
324             return false;
325         }
326
327         if (!evaluateXPath("/Response/Parameters//Parameter[@name='StatusMessage']/text()", xmlResponse).isEmpty()) {
328             logger.debug("Hayward Connection thing: getTelemetry XML response: {}", xmlResponse);
329             return false;
330         }
331
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);
337                 }
338             }
339         }
340         return true;
341     }
342
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);
352                     }
353                 }
354             }
355         }
356         return false;
357     }
358
359     private synchronized void initPolling(int initalDelay) {
360         pollTelemetryFuture = scheduler.scheduleWithFixedDelay(() -> {
361             try {
362                 if (commFailureCount >= 5) {
363                     commFailureCount = 0;
364                     clearPolling(pollTelemetryFuture);
365                     clearPolling(pollAlarmsFuture);
366                     initialize();
367                     return;
368                 }
369                 if (!(getTelemetryData())) {
370                     commFailureCount++;
371                     return;
372                 }
373                 updateStatus(ThingStatus.ONLINE);
374             } catch (HaywardException e) {
375                 logger.debug("Hayward Connection thing: Exception during poll: {}", e.getMessage());
376             } catch (InterruptedException e) {
377                 return;
378             }
379         }, initalDelay, config.telemetryPollTime, TimeUnit.SECONDS);
380         return;
381     }
382
383     private synchronized void initAlarmPolling(int initalDelay) {
384         pollAlarmsFuture = scheduler.scheduleWithFixedDelay(() -> {
385             try {
386                 getAlarmList();
387             } catch (HaywardException e) {
388                 logger.debug("Hayward Connection thing: Exception during poll: {}", e.getMessage());
389             }
390         }, initalDelay, config.alarmPollTime, TimeUnit.SECONDS);
391     }
392
393     private void clearPolling(@Nullable ScheduledFuture<?> pollJob) {
394         if (pollJob != null) {
395             pollJob.cancel(false);
396         }
397     }
398
399     @Nullable
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))) {
405                     return thing;
406                 }
407             }
408         }
409         return null;
410     }
411
412     public List<String> evaluateXPath(String xpathExp, String xmlResponse) {
413         List<String> values = new ArrayList<>();
414         try {
415             InputSource inputXML = new InputSource(new StringReader(xmlResponse));
416             XPath xPath = XPathFactory.newInstance().newXPath();
417             NodeList nodes = (NodeList) xPath.evaluate(xpathExp, inputXML, XPathConstants.NODESET);
418
419             for (int i = 0; i < nodes.getLength(); i++) {
420                 values.add(nodes.item(i).getNodeValue());
421             }
422         } catch (XPathExpressionException e) {
423             logger.warn("XPathExpression exception: {}", e.getMessage());
424         }
425         return values;
426     }
427
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);
434     }
435
436     public synchronized String httpXmlResponse(String urlParameters) throws HaywardException, InterruptedException {
437         String urlParameterslength = Integer.toString(urlParameters.length());
438         String statusMessage;
439
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());
444         }
445
446         for (int retry = 0; retry <= 2; retry++) {
447             try {
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();
451
452                 int status = httpResponse.getStatus();
453                 String xmlResponse = httpResponse.getContentAsString();
454
455                 if (status == 200) {
456                     List<String> statusMessages = evaluateXPath(
457                             "/Response/Parameters//Parameter[@name='StatusMessage']/text()", xmlResponse);
458                     if (!(statusMessages.isEmpty())) {
459                         statusMessage = statusMessages.get(0);
460                     } else {
461                         statusMessage = httpResponse.getReason();
462                     }
463
464                     if (logger.isTraceEnabled()) {
465                         logger.trace("Hayward Connection thing:  {} Hayward http response: {} {}", getCallingMethod(),
466                                 statusMessage, xmlResponse);
467                     }
468                     return xmlResponse;
469                 } else {
470                     if (logger.isDebugEnabled()) {
471                         logger.debug("Hayward Connection thing:  {} Hayward http response: {} {}", getCallingMethod(),
472                                 status, xmlResponse);
473                     }
474                     return "";
475                 }
476             } catch (ExecutionException e) {
477                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
478                         "Unable to resolve host.  Check Hayward hostname and your internet connection. "
479                                 + e.getMessage());
480                 return "";
481             } catch (TimeoutException e) {
482                 if (retry >= 2) {
483                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
484                             "Connection Timeout.  Check Hayward hostname and your internet connection. "
485                                     + e.getMessage());
486                     return "";
487                 } else {
488                     logger.warn("Hayward Connection thing Timeout:  {} Try:  {} ", getCallingMethod(), retry + 1);
489                 }
490             }
491         }
492         return "";
493     }
494
495     private String getCallingMethod() {
496         StackTraceElement[] stacktrace = Thread.currentThread().getStackTrace();
497         StackTraceElement e = stacktrace[3];
498         return e.getMethodName();
499     }
500
501     void updateChannelStateDescriptionFragment(Channel channel, StateDescriptionFragment descriptionFragment) {
502         ChannelUID channelId = channel.getUID();
503         stateDescriptionProvider.setStateDescriptionFragment(channelId, descriptionFragment);
504     }
505
506     public int convertCommand(Command command) {
507         if (command == OnOffType.ON) {
508             return 1;
509         } else {
510             return 0;
511         }
512     }
513 }