]> git.basschouten.com Git - openhab-addons.git/blob
fd09bddf0e9d6832ff8ec87ccd9ebed291ff7bbf
[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.List;
19 import java.util.Map;
20 import java.util.Set;
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 Set.of(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 = """
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>";
200
201         xmlResponse = httpXmlResponse(urlParameters);
202
203         if (xmlResponse.isEmpty()) {
204             logger.debug("Hayward Connection thing: Login XML response was null");
205             return false;
206         }
207
208         status = evaluateXPath("/Response/Parameters//Parameter[@name='Status']/text()", xmlResponse).get(0);
209
210         if (!("0".equals(status))) {
211             logger.debug("Hayward Connection thing: Login XML response: {}", xmlResponse);
212             return false;
213         }
214
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);
217         return true;
218     }
219
220     public synchronized boolean getApiDef() throws HaywardException, InterruptedException {
221         String xmlResponse;
222
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>";
231
232         xmlResponse = httpXmlResponse(urlParameters);
233
234         if (xmlResponse.isEmpty()) {
235             logger.debug("Hayward Connection thing: getApiDef XML response was null");
236             return false;
237         }
238         return true;
239     }
240
241     public synchronized boolean getSiteList() throws HaywardException, InterruptedException {
242         String xmlResponse;
243         String status;
244
245         // *****Get MSP
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>";
251
252         xmlResponse = httpXmlResponse(urlParameters);
253
254         if (xmlResponse.isEmpty()) {
255             logger.debug("Hayward Connection thing: getSiteList XML response was null");
256             return false;
257         }
258
259         status = evaluateXPath("/Response/Parameters//Parameter[@name='Status']/text()", xmlResponse).get(0);
260
261         if (!("0".equals(status))) {
262             logger.debug("Hayward Connection thing: getSiteList XML response: {}", xmlResponse);
263             return false;
264         }
265
266         account.mspSystemID = evaluateXPath("/Response/Parameters/Parameter/Item//Property[@name='MspSystemID']/text()",
267                 xmlResponse).get(0);
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()",
271                 xmlResponse).get(0);
272         return true;
273     }
274
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>";
283
284         String xmlResponse = httpXmlResponse(urlParameters);
285
286         if (xmlResponse.isEmpty()) {
287             logger.debug("Hayward Connection thing: getMSPConfig XML response was null");
288             return "Fail";
289         }
290
291         if (evaluateXPath("//Backyard/Name/text()", xmlResponse).isEmpty()) {
292             logger.debug("Hayward Connection thing: getMSPConfig XML response: {}", xmlResponse);
293             return "Fail";
294         }
295         return xmlResponse;
296     }
297
298     public synchronized boolean mspConfigUnits() throws HaywardException, InterruptedException {
299         List<String> property1 = new ArrayList<>();
300         List<String> property2 = new ArrayList<>();
301
302         String xmlResponse = getMspConfig();
303
304         if (xmlResponse.contentEquals("Fail")) {
305             return false;
306         }
307
308         // Get Units (Standard, Metric)
309         property1 = evaluateXPath("//System/Units/text()", xmlResponse);
310         account.units = property1.get(0);
311
312         // Get Variable Speed Pump Units (percent, RPM)
313         property2 = evaluateXPath("//System/Msp-Vsp-Speed-Format/text()", xmlResponse);
314         account.vspSpeedFormat = property2.get(0);
315
316         return true;
317     }
318
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>";
326
327         String xmlResponse = httpXmlResponse(urlParameters);
328
329         if (xmlResponse.isEmpty()) {
330             logger.debug("Hayward Connection thing: getTelemetry XML response was null");
331             return false;
332         }
333
334         if (!evaluateXPath("/Response/Parameters//Parameter[@name='StatusMessage']/text()", xmlResponse).isEmpty()) {
335             logger.debug("Hayward Connection thing: getTelemetry XML response: {}", xmlResponse);
336             return false;
337         }
338
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);
344                 }
345             }
346         }
347         return true;
348     }
349
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);
359                     }
360                 }
361             }
362         }
363         return false;
364     }
365
366     private synchronized void initPolling(int initalDelay) {
367         pollTelemetryFuture = scheduler.scheduleWithFixedDelay(() -> {
368             try {
369                 if (commFailureCount >= 5) {
370                     commFailureCount = 0;
371                     clearPolling(pollTelemetryFuture);
372                     clearPolling(pollAlarmsFuture);
373                     initialize();
374                     return;
375                 }
376                 if (!(getTelemetryData())) {
377                     commFailureCount++;
378                     return;
379                 }
380                 updateStatus(ThingStatus.ONLINE);
381             } catch (HaywardException e) {
382                 logger.debug("Hayward Connection thing: Exception during poll: {}", e.getMessage());
383             } catch (InterruptedException e) {
384                 return;
385             }
386         }, initalDelay, config.telemetryPollTime, TimeUnit.SECONDS);
387         return;
388     }
389
390     private synchronized void initAlarmPolling(int initalDelay) {
391         pollAlarmsFuture = scheduler.scheduleWithFixedDelay(() -> {
392             try {
393                 getAlarmList();
394             } catch (HaywardException e) {
395                 logger.debug("Hayward Connection thing: Exception during poll: {}", e.getMessage());
396             }
397         }, initalDelay, config.alarmPollTime, TimeUnit.SECONDS);
398     }
399
400     private void clearPolling(@Nullable ScheduledFuture<?> pollJob) {
401         if (pollJob != null) {
402             pollJob.cancel(false);
403         }
404     }
405
406     @Nullable
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))) {
412                     return thing;
413                 }
414             }
415         }
416         return null;
417     }
418
419     public List<String> evaluateXPath(String xpathExp, String xmlResponse) {
420         List<String> values = new ArrayList<>();
421         try {
422             InputSource inputXML = new InputSource(new StringReader(xmlResponse));
423             XPath xPath = XPathFactory.newInstance().newXPath();
424             NodeList nodes = (NodeList) xPath.evaluate(xpathExp, inputXML, XPathConstants.NODESET);
425
426             for (int i = 0; i < nodes.getLength(); i++) {
427                 values.add(nodes.item(i).getNodeValue());
428             }
429         } catch (XPathExpressionException e) {
430             logger.warn("XPathExpression exception: {}", e.getMessage());
431         }
432         return values;
433     }
434
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);
441     }
442
443     public synchronized String httpXmlResponse(String urlParameters) throws HaywardException, InterruptedException {
444         String urlParameterslength = Integer.toString(urlParameters.length());
445         String statusMessage;
446
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());
451         }
452
453         for (int retry = 0; retry <= 2; retry++) {
454             try {
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();
458
459                 int status = httpResponse.getStatus();
460                 String xmlResponse = httpResponse.getContentAsString();
461
462                 if (status == 200) {
463                     List<String> statusMessages = evaluateXPath(
464                             "/Response/Parameters//Parameter[@name='StatusMessage']/text()", xmlResponse);
465                     if (!(statusMessages.isEmpty())) {
466                         statusMessage = statusMessages.get(0);
467                     } else {
468                         statusMessage = httpResponse.getReason();
469                     }
470
471                     if (logger.isTraceEnabled()) {
472                         logger.trace("Hayward Connection thing:  {} Hayward http response: {} {}", getCallingMethod(),
473                                 statusMessage, xmlResponse);
474                     }
475                     return xmlResponse;
476                 } else {
477                     if (logger.isDebugEnabled()) {
478                         logger.debug("Hayward Connection thing:  {} Hayward http response: {} {}", getCallingMethod(),
479                                 status, xmlResponse);
480                     }
481                     return "";
482                 }
483             } catch (ExecutionException e) {
484                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
485                         "Unable to resolve host.  Check Hayward hostname and your internet connection. "
486                                 + e.getMessage());
487                 return "";
488             } catch (TimeoutException e) {
489                 if (retry >= 2) {
490                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
491                             "Connection Timeout.  Check Hayward hostname and your internet connection. "
492                                     + e.getMessage());
493                     return "";
494                 } else {
495                     logger.warn("Hayward Connection thing Timeout:  {} Try:  {} ", getCallingMethod(), retry + 1);
496                 }
497             }
498         }
499         return "";
500     }
501
502     private String getCallingMethod() {
503         StackTraceElement[] stacktrace = Thread.currentThread().getStackTrace();
504         StackTraceElement e = stacktrace[3];
505         return e.getMethodName();
506     }
507
508     void updateChannelStateDescriptionFragment(Channel channel, StateDescriptionFragment descriptionFragment) {
509         ChannelUID channelId = channel.getUID();
510         stateDescriptionProvider.setStateDescriptionFragment(channelId, descriptionFragment);
511     }
512
513     public int convertCommand(Command command) {
514         if (command == OnOffType.ON) {
515             return 1;
516         } else {
517             return 0;
518         }
519     }
520 }