]> git.basschouten.com Git - openhab-addons.git/blob
25c12aab361406e7acd4b90ff896731233b540a9
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.helios.internal.handler;
14
15 import static org.openhab.binding.helios.internal.HeliosBindingConstants.*;
16
17 import java.math.BigDecimal;
18 import java.text.SimpleDateFormat;
19 import java.util.ArrayList;
20 import java.util.Calendar;
21 import java.util.GregorianCalendar;
22 import java.util.List;
23 import java.util.concurrent.RejectedExecutionException;
24 import java.util.concurrent.TimeUnit;
25
26 import javax.jws.WebParam;
27 import javax.jws.WebService;
28 import javax.xml.bind.JAXBContext;
29 import javax.xml.bind.JAXBElement;
30 import javax.xml.bind.JAXBException;
31 import javax.xml.namespace.QName;
32 import javax.xml.transform.dom.DOMResult;
33 import javax.xml.ws.BindingProvider;
34 import javax.xml.ws.BindingType;
35 import javax.xml.ws.Endpoint;
36 import javax.xml.ws.Service;
37 import javax.xml.ws.WebServiceException;
38 import javax.xml.ws.handler.Handler;
39 import javax.xml.ws.wsaddressing.W3CEndpointReference;
40 import javax.xml.ws.wsaddressing.W3CEndpointReferenceBuilder;
41
42 import org.apache.cxf.helpers.DOMUtils;
43 import org.apache.cxf.wsn.wsdl.WSNWSDLLocator;
44 import org.oasis_open.docs.wsn.b_2.FilterType;
45 import org.oasis_open.docs.wsn.b_2.Notify;
46 import org.oasis_open.docs.wsn.b_2.Renew;
47 import org.oasis_open.docs.wsn.b_2.RenewResponse;
48 import org.oasis_open.docs.wsn.b_2.Subscribe;
49 import org.oasis_open.docs.wsn.b_2.SubscribeResponse;
50 import org.oasis_open.docs.wsn.b_2.TopicExpressionType;
51 import org.oasis_open.docs.wsn.b_2.Unsubscribe;
52 import org.oasis_open.docs.wsn.bw_2.InvalidFilterFault;
53 import org.oasis_open.docs.wsn.bw_2.InvalidMessageContentExpressionFault;
54 import org.oasis_open.docs.wsn.bw_2.InvalidProducerPropertiesExpressionFault;
55 import org.oasis_open.docs.wsn.bw_2.InvalidTopicExpressionFault;
56 import org.oasis_open.docs.wsn.bw_2.NotificationConsumer;
57 import org.oasis_open.docs.wsn.bw_2.NotificationProducer;
58 import org.oasis_open.docs.wsn.bw_2.NotifyMessageNotSupportedFault;
59 import org.oasis_open.docs.wsn.bw_2.PausableSubscriptionManager;
60 import org.oasis_open.docs.wsn.bw_2.SubscribeCreationFailedFault;
61 import org.oasis_open.docs.wsn.bw_2.TopicExpressionDialectUnknownFault;
62 import org.oasis_open.docs.wsn.bw_2.TopicNotSupportedFault;
63 import org.oasis_open.docs.wsn.bw_2.UnableToDestroySubscriptionFault;
64 import org.oasis_open.docs.wsn.bw_2.UnacceptableInitialTerminationTimeFault;
65 import org.oasis_open.docs.wsn.bw_2.UnacceptableTerminationTimeFault;
66 import org.oasis_open.docs.wsn.bw_2.UnrecognizedPolicyRequestFault;
67 import org.oasis_open.docs.wsn.bw_2.UnsupportedPolicyRequestFault;
68 import org.oasis_open.docs.wsrf.rw_2.ResourceUnknownFault;
69 import org.openhab.binding.helios.internal.ws.soap.SOAPActionHandler;
70 import org.openhab.binding.helios.internal.ws.soap.SOAPCallStateChanged;
71 import org.openhab.binding.helios.internal.ws.soap.SOAPCardEntered;
72 import org.openhab.binding.helios.internal.ws.soap.SOAPCodeEntered;
73 import org.openhab.binding.helios.internal.ws.soap.SOAPDeviceState;
74 import org.openhab.binding.helios.internal.ws.soap.SOAPEvent;
75 import org.openhab.binding.helios.internal.ws.soap.SOAPKeyPressed;
76 import org.openhab.binding.helios.internal.ws.soap.SOAPObjectFactory;
77 import org.openhab.binding.helios.internal.ws.soap.SOAPSubscriptionActionHandler;
78 import org.openhab.core.library.types.DateTimeType;
79 import org.openhab.core.library.types.StringType;
80 import org.openhab.core.thing.ChannelUID;
81 import org.openhab.core.thing.Thing;
82 import org.openhab.core.thing.ThingStatus;
83 import org.openhab.core.thing.ThingStatusDetail;
84 import org.openhab.core.thing.binding.BaseThingHandler;
85 import org.openhab.core.types.Command;
86 import org.openhab.core.types.RefreshType;
87 import org.slf4j.Logger;
88 import org.slf4j.LoggerFactory;
89 import org.w3c.dom.Element;
90 import org.w3c.dom.NodeList;
91
92 /**
93  * The {@link HeliosHandler27} is responsible for handling commands, which are
94  * sent to one of the channels.
95  *
96  * @author Karel Goderis - Initial contribution
97  */
98 @WebService(endpointInterface = "org.oasis_open.docs.wsn.bw_2.NotificationConsumer")
99 @BindingType(javax.xml.ws.soap.SOAPBinding.SOAP12HTTP_BINDING)
100 public class HeliosHandler27 extends BaseThingHandler implements NotificationConsumer {
101
102     // List of Configuration constants
103     public static final String IP_ADDRESS = "ipAddress";
104     public static final String OPENHAB_IP_ADDRESS = "openHABipAddress";
105     public static final String OPENHAB_PORT_NUMBER = "openHABportNumber";
106
107     private final Logger logger = LoggerFactory.getLogger(HeliosHandler27.class);
108
109     private static final String SUBSCRIPTION_PERIOD = "PT1H";
110
111     private JAXBContext context = null;
112     private Endpoint endpoint = null;
113     private NotificationProducer notificationProducer = null;
114     private PausableSubscriptionManager subscription = null;
115     private GregorianCalendar currentTime;
116     private GregorianCalendar terminationTime;
117     private W3CEndpointReference subscriptionReference;
118     private String subscriptionID;
119     private SOAPEvent previousEvent = null;
120
121     public static final String HELIOS_URI = "http://www.2n.cz/2013/event";
122     public static final String DIALECT_URI = "http://www.2n.cz/2013/TopicExpression/Multiple";
123     public static final String WSN_URI = "http://docs.oasis-open.org/wsn/b-2";
124
125     public static final QName TOPIC_EXPRESSION = new QName(WSN_URI, "TopicExpression");
126     public static final QName INITIAL_TERMINATION_TIME = new QName(WSN_URI, "InitialTerminationTime");
127     public static final QName MAXIMUM_NUMBER = new QName(HELIOS_URI, "MaximumNumber");
128     public static final QName SIMPLE_MESSAGES = new QName(HELIOS_URI, "SimpleMessages");
129     public static final QName START_TIME_STAMP = new QName(HELIOS_URI, "StartTimestamp");
130     public static final QName START_RECORD_ID = new QName(HELIOS_URI, "StartRecordId");
131
132     public HeliosHandler27(Thing thing) {
133         super(thing);
134     }
135
136     @Override
137     public void handleCommand(ChannelUID channelUID, Command command) {
138         if (!(command instanceof RefreshType)) {
139             // 2N.cz has not yet released the automation part of the Helios IP HTTP/SOAP based API. Only the
140             // notification part has been documented, so for now there is nothing to do
141             // here
142             logger.debug("The Helios IP is a read-only device and can not handle commands");
143         }
144     }
145
146     public String getSubscriptionID() {
147         return subscriptionID;
148     }
149
150     @SuppressWarnings("rawtypes")
151     @Override
152     public void initialize() {
153         logger.debug("Initializing Helios IP handler.");
154         List<Handler> handlerChain = new ArrayList<>();
155         handlerChain.add(new SOAPActionHandler());
156
157         try {
158             context = JAXBContext.newInstance(SOAPObjectFactory.class, SOAPEvent.class, SOAPKeyPressed.class,
159                     SOAPCallStateChanged.class, SOAPCodeEntered.class, SOAPCardEntered.class, SOAPDeviceState.class);
160         } catch (JAXBException e) {
161             logger.error("An exception occurred while setting up the JAXB Context factory: {}", e.getMessage(), e);
162             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
163             throw new RuntimeException();
164         }
165
166         try {
167             if (endpoint == null || (endpoint != null && !endpoint.isPublished())) {
168                 String address = "http://" + (String) getConfig().get(OPENHAB_IP_ADDRESS) + ":"
169                         + ((BigDecimal) getConfig().get(OPENHAB_PORT_NUMBER)).toString() + "/notification"
170                         + System.currentTimeMillis();
171                 logger.debug("Publishing the notification consumer webservice on '{}", address);
172                 endpoint = Endpoint.publish(address, this);
173                 ((javax.xml.ws.soap.SOAPBinding) endpoint.getBinding()).setHandlerChain(handlerChain);
174             }
175         } catch (WebServiceException e1) {
176             logger.debug("An exception occurred while setting up the notification consumer webservice: {}",
177                     e1.getMessage(), e1);
178             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e1.getMessage());
179             return;
180         }
181
182         try {
183             String heliosAddress = "http://" + (String) getConfig().get(IP_ADDRESS) + "/notification";
184             Service notificationProducerService = Service.create(WSNWSDLLocator.getWSDLUrl(),
185                     new QName("http://cxf.apache.org/wsn/jaxws", "NotificationProducerService"));
186             notificationProducer = notificationProducerService.getPort(
187                     new W3CEndpointReferenceBuilder().address(heliosAddress).build(), NotificationProducer.class);
188             ((BindingProvider) notificationProducer).getBinding().setHandlerChain(handlerChain);
189         } catch (WebServiceException e1) {
190             logger.debug("An exception occurred while setting up the notification webservice client: {}",
191                     e1.getMessage(), e1);
192             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e1.getMessage());
193             return;
194         }
195
196         try {
197             // set up the access to the subscription manager on the Helios IP
198             // Vario so that we can renew in the future
199             String heliosAddress = "http://" + (String) getConfig().get(IP_ADDRESS) + "/notification";
200             Service subscriptionService = Service.create(WSNWSDLLocator.getWSDLUrl(),
201                     new QName("http://cxf.apache.org/wsn/jaxws", "PausableSubscriptionManagerService"));
202             subscription = subscriptionService.getPort(new W3CEndpointReferenceBuilder().address(heliosAddress).build(),
203                     PausableSubscriptionManager.class);
204
205             handlerChain = new ArrayList<>();
206             handlerChain.add(new SOAPSubscriptionActionHandler(this));
207             ((BindingProvider) subscription).getBinding().setHandlerChain(handlerChain);
208         } catch (WebServiceException e1) {
209             logger.debug("An exception occurred while setting up the subscription manager client: {}", e1.getMessage(),
210                     e1);
211             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e1.getMessage());
212             return;
213         }
214
215         try {
216             subscribe(endpoint.getEndpointReference(W3CEndpointReference.class));
217         } catch (WebServiceException | TopicNotSupportedFault | InvalidFilterFault | TopicExpressionDialectUnknownFault
218                 | UnacceptableInitialTerminationTimeFault | SubscribeCreationFailedFault
219                 | InvalidMessageContentExpressionFault | InvalidTopicExpressionFault | UnrecognizedPolicyRequestFault
220                 | UnsupportedPolicyRequestFault | ResourceUnknownFault | NotifyMessageNotSupportedFault
221                 | InvalidProducerPropertiesExpressionFault e) {
222             logger.debug("An exception occurred while subscribing to the notifications for thing '{}': {}",
223                     getThing().getUID(), e.getMessage(), e);
224             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
225             return;
226         }
227
228         updateStatus(ThingStatus.ONLINE);
229     }
230
231     @Override
232     public void dispose() {
233         logger.debug("Disposing Helios IP handler.");
234
235         try {
236             unsubscribe();
237             if (endpoint != null) {
238                 endpoint.stop();
239             }
240             endpoint = null;
241             terminationTime = null;
242             context = null;
243             notificationProducer = null;
244             subscription = null;
245         } catch (Exception e) {
246             logger.error("An exception occurred while disposing the Helios Thing Handler : {}", e.getMessage(), e);
247         }
248     }
249
250     @Override
251     public void notify(
252             @WebParam(partName = "Notify", name = "Notify", targetNamespace = "http://docs.oasis-open.org/wsn/b-2") Notify notify) {
253         for (Object object : notify.getAny()) {
254             try {
255                 this.processNotification(context.createUnmarshaller().unmarshal((Element) object, SOAPEvent.class));
256             } catch (JAXBException e) {
257                 logger.error("An exception occurred while processing a notification message : {}", e.getMessage(), e);
258             }
259         }
260     }
261
262     public void processNotification(JAXBElement<SOAPEvent> message) {
263         if (getThing().getStatus() == ThingStatus.ONLINE) {
264             SOAPEvent event = message.getValue();
265             // WS-Notification does not provide a mechanism to query existing
266             // subscriptions established before, so these keep lingering on the
267             // remote device. Therefore, when restarting the OH runtime, we
268             // might receive events more than one time, we need to filter these
269             // out
270             if (previousEvent == null || !previousEvent.equals(event)) {
271                 previousEvent = event;
272
273                 Object data = event.getData();
274
275                 if (data instanceof SOAPKeyPressed) {
276                     StringType valueType = new StringType(((SOAPKeyPressed) data).getKeyCode());
277                     updateState(new ChannelUID(getThing().getUID(), KEY_PRESSED), valueType);
278
279                     DateTimeType stampType = new DateTimeType(event.getTimestamp());
280                     updateState(new ChannelUID(getThing().getUID(), KEY_PRESSED_STAMP), stampType);
281                 }
282
283                 if (data instanceof SOAPCallStateChanged) {
284                     StringType valueType = new StringType(((SOAPCallStateChanged) data).getState());
285                     updateState(new ChannelUID(getThing().getUID(), CALL_STATE), valueType);
286
287                     valueType = new StringType(((SOAPCallStateChanged) data).getDirection());
288                     updateState(new ChannelUID(getThing().getUID(), CALL_DIRECTION), valueType);
289
290                     DateTimeType stampType = new DateTimeType(event.getTimestamp());
291                     updateState(new ChannelUID(getThing().getUID(), CALL_STATE_STAMP), stampType);
292                 }
293
294                 if (data instanceof SOAPCardEntered) {
295                     StringType valueType = new StringType(((SOAPCardEntered) data).getCard());
296                     updateState(new ChannelUID(getThing().getUID(), CARD), valueType);
297
298                     valueType = new StringType(((SOAPCardEntered) data).getValid());
299                     updateState(new ChannelUID(getThing().getUID(), CARD_VALID), valueType);
300
301                     DateTimeType stampType = new DateTimeType(event.getTimestamp());
302                     updateState(new ChannelUID(getThing().getUID(), CARD_STAMP), stampType);
303                 }
304
305                 if (data instanceof SOAPCodeEntered) {
306                     StringType valueType = new StringType(((SOAPCodeEntered) data).getCode());
307                     updateState(new ChannelUID(getThing().getUID(), CODE), valueType);
308
309                     valueType = new StringType(((SOAPCodeEntered) data).getValid());
310                     updateState(new ChannelUID(getThing().getUID(), CODE_VALID), valueType);
311
312                     DateTimeType stampType = new DateTimeType(event.getTimestamp());
313                     updateState(new ChannelUID(getThing().getUID(), CODE_STAMP), stampType);
314                 }
315
316                 if (data instanceof SOAPDeviceState) {
317                     StringType valueType = new StringType(((SOAPDeviceState) data).getState());
318                     updateState(new ChannelUID(getThing().getUID(), DEVICE_STATE), valueType);
319
320                     DateTimeType stampType = new DateTimeType(event.getTimestamp());
321                     updateState(new ChannelUID(getThing().getUID(), DEVICE_STATE_STAMP), stampType);
322                 }
323             } else {
324                 logger.warn("Duplicate event received due to lingering subscriptions: '{}':'{}'", event.getEventName(),
325                         event.getTimestamp());
326             }
327         }
328     }
329
330     public void renew(String newTerminationTime) throws ResourceUnknownFault, UnacceptableTerminationTimeFault {
331         if (subscription != null) {
332             Renew renew = new Renew();
333             renew.setTerminationTime(newTerminationTime);
334
335             RenewResponse response = subscription.renew(renew);
336             currentTime = response.getCurrentTime().toGregorianCalendar();
337             terminationTime = response.getTerminationTime().toGregorianCalendar();
338
339             SimpleDateFormat pFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
340             logger.debug("Renewed the subscription with ID '{}' for '{}' from {} until {}",
341                     new Object[] { getSubscriptionID(), getThing().getUID(), pFormatter.format(currentTime.getTime()),
342                             pFormatter.format(terminationTime.getTime()) });
343         }
344     }
345
346     public void unsubscribe() {
347         if (subscription != null) {
348             try {
349                 subscription.unsubscribe(new Unsubscribe());
350                 logger.debug("Unsubscribing the subscription with ID '{}' for '{}' ", getSubscriptionID(),
351                         getThing().getUID());
352             } catch (UnableToDestroySubscriptionFault | ResourceUnknownFault e) {
353                 logger.error("An exception occurred while unsubscribing from the subscription : {}", e.getMessage(), e);
354             }
355         }
356     }
357
358     public void subscribe(W3CEndpointReference epr)
359             throws TopicNotSupportedFault, InvalidFilterFault, TopicExpressionDialectUnknownFault,
360             UnacceptableInitialTerminationTimeFault, SubscribeCreationFailedFault, InvalidMessageContentExpressionFault,
361             InvalidTopicExpressionFault, UnrecognizedPolicyRequestFault, UnsupportedPolicyRequestFault,
362             ResourceUnknownFault, NotifyMessageNotSupportedFault, InvalidProducerPropertiesExpressionFault {
363         if (notificationProducer != null) {
364             Subscribe subscribeRequest = new Subscribe();
365             subscribeRequest.setConsumerReference(epr);
366             subscribeRequest.setFilter(new FilterType());
367
368             TopicExpressionType topicExp = new TopicExpressionType();
369             topicExp.getContent().add("");
370             topicExp.setDialect(DIALECT_URI);
371             subscribeRequest.getFilter().getAny()
372                     .add(new JAXBElement<>(TOPIC_EXPRESSION, TopicExpressionType.class, topicExp));
373
374             subscribeRequest.setInitialTerminationTime(
375                     new JAXBElement<>(INITIAL_TERMINATION_TIME, String.class, SUBSCRIPTION_PERIOD));
376
377             subscribeRequest.setSubscriptionPolicy(new Subscribe.SubscriptionPolicy());
378             subscribeRequest.getSubscriptionPolicy().getAny().add(new JAXBElement<>(MAXIMUM_NUMBER, Integer.class, 1));
379             subscribeRequest.getSubscriptionPolicy().getAny().add(new JAXBElement<>(SIMPLE_MESSAGES, Integer.class, 1));
380             GregorianCalendar now = new GregorianCalendar();
381             SimpleDateFormat pFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
382             subscribeRequest.getSubscriptionPolicy().getAny()
383                     .add(new JAXBElement<>(START_TIME_STAMP, String.class, pFormatter.format(now.getTime())));
384
385             SubscribeResponse response = notificationProducer.subscribe(subscribeRequest);
386
387             if (response != null) {
388                 currentTime = response.getCurrentTime().toGregorianCalendar();
389                 terminationTime = response.getTerminationTime().toGregorianCalendar();
390                 subscriptionReference = response.getSubscriptionReference();
391
392                 Element element = DOMUtils.createDocument().createElement("elem");
393                 subscriptionReference.writeTo(new DOMResult(element));
394                 NodeList nl = element.getElementsByTagNameNS("http://www.2n.cz/2013/event", "SubscriptionId");
395                 if (nl != null && nl.getLength() > 0) {
396                     Element e = (Element) nl.item(0);
397                     subscriptionID = DOMUtils.getContent(e).trim();
398                 }
399                 logger.debug("Established a subscription with ID '{}' for '{}' as from {} until {}",
400                         new Object[] { subscriptionID, getThing().getUID(), pFormatter.format(currentTime.getTime()),
401                                 pFormatter.format(terminationTime.getTime()) });
402
403                 java.util.Calendar triggerTime = terminationTime;
404                 triggerTime.add(Calendar.MINUTE, -1);
405
406                 logger.debug("Scheduling a renewal of the subscription with ID '{}' for '{}' to happen on {}",
407                         new Object[] { subscriptionID, getThing().getUID(), pFormatter.format(triggerTime.getTime()) });
408                 try {
409                     scheduler.schedule(renewRunnable, triggerTime.getTimeInMillis() - System.currentTimeMillis(),
410                             TimeUnit.MILLISECONDS);
411                 } catch (RejectedExecutionException e) {
412                     logger.error("An exception occurred while scheduling a renewal : '{}'", e.getMessage(), e);
413                 }
414             }
415         }
416     }
417
418     protected Runnable renewRunnable = new Runnable() {
419
420         @Override
421         public void run() {
422             SimpleDateFormat pFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
423             boolean result = false;
424
425             try {
426                 ((HeliosHandler27) getThing().getHandler()).renew(SUBSCRIPTION_PERIOD);
427                 result = true;
428             } catch (Exception e) {
429                 logger.error("An exception occurred while renewing the subscription : {}", e.getMessage(), e);
430                 ((HeliosHandler27) getThing().getHandler()).dispose();
431                 ((HeliosHandler27) getThing().getHandler()).initialize();
432                 return;
433             }
434
435             if (result) {
436                 java.util.Calendar triggerTime = terminationTime;
437                 triggerTime.add(Calendar.MINUTE, -1);
438
439                 logger.debug("Scheduling a renewal of the subscription with ID '{}' for '{}' to happen on {}",
440                         new Object[] { subscriptionID, getThing().getUID(), pFormatter.format(triggerTime.getTime()) });
441                 try {
442                     scheduler.schedule(renewRunnable, triggerTime.getTimeInMillis() - System.currentTimeMillis(),
443                             TimeUnit.MILLISECONDS);
444                 } catch (RejectedExecutionException e) {
445                     logger.error("An exception occurred while scheduling a renewal : '{}'", e.getMessage(), e);
446                 }
447             }
448         }
449     };
450 }