]> git.basschouten.com Git - openhab-addons.git/blob
3c52d51f12ade2f4f59af56e23f9935fd65987dd
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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             return false;
203         }
204
205         status = evaluateXPath("/Response/Parameters//Parameter[@name='Status']/text()", xmlResponse).get(0);
206
207         if (!("0".equals(status))) {
208             logger.debug("Hayward Connection thing: Login XML response: {}", xmlResponse);
209             return false;
210         }
211
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);
214         return true;
215     }
216
217     public synchronized boolean getApiDef() throws HaywardException, InterruptedException {
218         String xmlResponse;
219
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>";
226
227         xmlResponse = httpXmlResponse(urlParameters);
228
229         if (xmlResponse.isEmpty()) {
230             logger.debug("Hayward Connection thing: Login XML response was null");
231             return false;
232         }
233         return true;
234     }
235
236     public synchronized boolean getSiteList() throws HaywardException, InterruptedException {
237         String xmlResponse;
238         String status;
239
240         // *****Get MSP
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>";
245
246         xmlResponse = httpXmlResponse(urlParameters);
247
248         if (xmlResponse.isEmpty()) {
249             logger.debug("Hayward Connection thing: getSiteList XML response was null");
250             return false;
251         }
252
253         status = evaluateXPath("/Response/Parameters//Parameter[@name='Status']/text()", xmlResponse).get(0);
254
255         if (!("0".equals(status))) {
256             logger.debug("Hayward Connection thing: getSiteList XML response: {}", xmlResponse);
257             return false;
258         }
259
260         account.mspSystemID = evaluateXPath("/Response/Parameters/Parameter/Item//Property[@name='MspSystemID']/text()",
261                 xmlResponse).get(0);
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()",
265                 xmlResponse).get(0);
266         return true;
267     }
268
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>";
276
277         String xmlResponse = httpXmlResponse(urlParameters);
278
279         if (xmlResponse.isEmpty()) {
280             logger.debug("Hayward Connection thing: requestConfig XML response was null");
281             return "Fail";
282         }
283
284         if (evaluateXPath("//Backyard/Name/text()", xmlResponse).isEmpty()) {
285             logger.debug("Hayward Connection thing: requestConfiguration XML response: {}", xmlResponse);
286             return "Fail";
287         }
288         return xmlResponse;
289     }
290
291     public synchronized boolean mspConfigUnits() throws HaywardException, InterruptedException {
292         List<String> property1 = new ArrayList<>();
293         List<String> property2 = new ArrayList<>();
294
295         String xmlResponse = getMspConfig();
296
297         // Get Units (Standard, Metric)
298         property1 = evaluateXPath("//System/Units/text()", xmlResponse);
299         account.units = property1.get(0);
300
301         // Get Variable Speed Pump Units (percent, RPM)
302         property2 = evaluateXPath("//System/Msp-Vsp-Speed-Format/text()", xmlResponse);
303         account.vspSpeedFormat = property2.get(0);
304
305         return true;
306     }
307
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>";
314
315         String xmlResponse = httpXmlResponse(urlParameters);
316
317         if (xmlResponse.isEmpty()) {
318             logger.debug("Hayward Connection thing: getTelemetry XML response was null");
319             return false;
320         }
321
322         if (!evaluateXPath("/Response/Parameters//Parameter[@name='StatusMessage']/text()", xmlResponse).isEmpty()) {
323             logger.debug("Hayward Connection thing: getTelemetry XML response: {}", xmlResponse);
324             return false;
325         }
326
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);
332                 }
333             }
334         }
335         return true;
336     }
337
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);
347                     }
348                 }
349             }
350         }
351         return false;
352     }
353
354     private synchronized void initPolling(int initalDelay) {
355         pollTelemetryFuture = scheduler.scheduleWithFixedDelay(() -> {
356             try {
357                 if (commFailureCount >= 5) {
358                     commFailureCount = 0;
359                     clearPolling(pollTelemetryFuture);
360                     clearPolling(pollAlarmsFuture);
361                     initialize();
362                     return;
363                 }
364                 if (!(getTelemetryData())) {
365                     commFailureCount++;
366                     return;
367                 }
368                 updateStatus(ThingStatus.ONLINE);
369             } catch (HaywardException e) {
370                 logger.debug("Hayward Connection thing: Exception during poll: {}", e.getMessage());
371             } catch (InterruptedException e) {
372                 return;
373             }
374         }, initalDelay, config.telemetryPollTime, TimeUnit.SECONDS);
375         return;
376     }
377
378     private synchronized void initAlarmPolling(int initalDelay) {
379         pollAlarmsFuture = scheduler.scheduleWithFixedDelay(() -> {
380             try {
381                 getAlarmList();
382             } catch (HaywardException e) {
383                 logger.debug("Hayward Connection thing: Exception during poll: {}", e.getMessage());
384             }
385         }, initalDelay, config.alarmPollTime, TimeUnit.SECONDS);
386     }
387
388     private void clearPolling(@Nullable ScheduledFuture<?> pollJob) {
389         if (pollJob != null) {
390             pollJob.cancel(false);
391         }
392     }
393
394     @Nullable
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))) {
400                     return thing;
401                 }
402             }
403         }
404         return null;
405     }
406
407     public List<String> evaluateXPath(String xpathExp, String xmlResponse) {
408         List<String> values = new ArrayList<>();
409         try {
410             InputSource inputXML = new InputSource(new StringReader(xmlResponse));
411             XPath xPath = XPathFactory.newInstance().newXPath();
412             NodeList nodes = (NodeList) xPath.evaluate(xpathExp, inputXML, XPathConstants.NODESET);
413
414             for (int i = 0; i < nodes.getLength(); i++) {
415                 values.add(nodes.item(i).getNodeValue());
416             }
417         } catch (XPathExpressionException e) {
418             logger.warn("XPathExpression exception: {}", e.getMessage());
419         }
420         return values;
421     }
422
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);
429     }
430
431     public synchronized String httpXmlResponse(String urlParameters) throws HaywardException, InterruptedException {
432         String urlParameterslength = Integer.toString(urlParameters.length());
433         String statusMessage;
434
435         try {
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();
439
440             int status = httpResponse.getStatus();
441             String xmlResponse = httpResponse.getContentAsString();
442
443             if (status == 200) {
444                 List<String> statusMessages = evaluateXPath(
445                         "/Response/Parameters//Parameter[@name='StatusMessage']/text()", xmlResponse);
446                 if (!(statusMessages.isEmpty())) {
447                     statusMessage = statusMessages.get(0);
448                 } else {
449                     statusMessage = httpResponse.getReason();
450                 }
451
452                 if (logger.isTraceEnabled()) {
453                     logger.trace("Hayward Connection thing:  {} Hayward http command: {}", getCallingMethod(),
454                             urlParameters);
455                     logger.trace("Hayward Connection thing:  {} Hayward http response: {} {}", getCallingMethod(),
456                             statusMessage, xmlResponse);
457                 }
458                 return xmlResponse;
459             } else {
460                 if (logger.isDebugEnabled()) {
461                     logger.debug("Hayward Connection thing:  {} Hayward http command: {}", getCallingMethod(),
462                             urlParameters);
463                     logger.debug("Hayward Connection thing:  {} Hayward http response: {} {}", getCallingMethod(),
464                             status, xmlResponse);
465                 }
466                 return "";
467             }
468         } catch (ExecutionException e) {
469             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
470                     "Unable to resolve host.  Check Hayward hostname and your internet connection. " + e);
471             return "";
472         } catch (TimeoutException e) {
473             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
474                     "Connection Timeout.  Check Hayward hostname and your internet connection. " + e);
475             return "";
476         }
477     }
478
479     private String getCallingMethod() {
480         StackTraceElement[] stacktrace = Thread.currentThread().getStackTrace();
481         StackTraceElement e = stacktrace[3];
482         return e.getMethodName();
483     }
484
485     void updateChannelStateDescriptionFragment(Channel channel, StateDescriptionFragment descriptionFragment) {
486         ChannelUID channelId = channel.getUID();
487         stateDescriptionProvider.setStateDescriptionFragment(channelId, descriptionFragment);
488     }
489
490     public int convertCommand(Command command) {
491         if (command == OnOffType.ON) {
492             return 1;
493         } else {
494             return 0;
495         }
496     }
497 }