2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.helios.internal.handler;
15 import static org.openhab.binding.helios.internal.HeliosBindingConstants.*;
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;
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;
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.b_2.UnsubscribeResponse;
53 import org.oasis_open.docs.wsn.bw_2.InvalidFilterFault;
54 import org.oasis_open.docs.wsn.bw_2.InvalidMessageContentExpressionFault;
55 import org.oasis_open.docs.wsn.bw_2.InvalidProducerPropertiesExpressionFault;
56 import org.oasis_open.docs.wsn.bw_2.InvalidTopicExpressionFault;
57 import org.oasis_open.docs.wsn.bw_2.NotificationConsumer;
58 import org.oasis_open.docs.wsn.bw_2.NotificationProducer;
59 import org.oasis_open.docs.wsn.bw_2.NotifyMessageNotSupportedFault;
60 import org.oasis_open.docs.wsn.bw_2.PausableSubscriptionManager;
61 import org.oasis_open.docs.wsn.bw_2.SubscribeCreationFailedFault;
62 import org.oasis_open.docs.wsn.bw_2.TopicExpressionDialectUnknownFault;
63 import org.oasis_open.docs.wsn.bw_2.TopicNotSupportedFault;
64 import org.oasis_open.docs.wsn.bw_2.UnableToDestroySubscriptionFault;
65 import org.oasis_open.docs.wsn.bw_2.UnacceptableInitialTerminationTimeFault;
66 import org.oasis_open.docs.wsn.bw_2.UnacceptableTerminationTimeFault;
67 import org.oasis_open.docs.wsn.bw_2.UnrecognizedPolicyRequestFault;
68 import org.oasis_open.docs.wsn.bw_2.UnsupportedPolicyRequestFault;
69 import org.oasis_open.docs.wsrf.rw_2.ResourceUnknownFault;
70 import org.openhab.binding.helios.internal.ws.soap.SOAPActionHandler;
71 import org.openhab.binding.helios.internal.ws.soap.SOAPCallStateChanged;
72 import org.openhab.binding.helios.internal.ws.soap.SOAPCardEntered;
73 import org.openhab.binding.helios.internal.ws.soap.SOAPCodeEntered;
74 import org.openhab.binding.helios.internal.ws.soap.SOAPDeviceState;
75 import org.openhab.binding.helios.internal.ws.soap.SOAPEvent;
76 import org.openhab.binding.helios.internal.ws.soap.SOAPKeyPressed;
77 import org.openhab.binding.helios.internal.ws.soap.SOAPObjectFactory;
78 import org.openhab.binding.helios.internal.ws.soap.SOAPSubscriptionActionHandler;
79 import org.openhab.core.library.types.DateTimeType;
80 import org.openhab.core.library.types.StringType;
81 import org.openhab.core.thing.ChannelUID;
82 import org.openhab.core.thing.Thing;
83 import org.openhab.core.thing.ThingStatus;
84 import org.openhab.core.thing.ThingStatusDetail;
85 import org.openhab.core.thing.binding.BaseThingHandler;
86 import org.openhab.core.types.Command;
87 import org.openhab.core.types.RefreshType;
88 import org.slf4j.Logger;
89 import org.slf4j.LoggerFactory;
90 import org.w3c.dom.Element;
91 import org.w3c.dom.NodeList;
94 * The {@link HeliosHandler27} is responsible for handling commands, which are
95 * sent to one of the channels.
97 * @author Karel Goderis - Initial contribution
99 @WebService(endpointInterface = "org.oasis_open.docs.wsn.bw_2.NotificationConsumer")
100 @BindingType(javax.xml.ws.soap.SOAPBinding.SOAP12HTTP_BINDING)
101 public class HeliosHandler27 extends BaseThingHandler implements NotificationConsumer {
103 // List of Configuration constants
104 public static final String IP_ADDRESS = "ipAddress";
105 public static final String OPENHAB_IP_ADDRESS = "openHABipAddress";
106 public static final String OPENHAB_PORT_NUMBER = "openHABportNumber";
108 private final Logger logger = LoggerFactory.getLogger(HeliosHandler27.class);
110 private static final String SUBSCRIPTION_PERIOD = "PT1H";
112 private JAXBContext context = null;
113 private Endpoint endpoint = null;
114 private NotificationProducer notificationProducer = null;
115 private PausableSubscriptionManager subscription = null;
116 private GregorianCalendar currentTime;
117 private GregorianCalendar terminationTime;
118 private W3CEndpointReference subscriptionReference;
119 private String subscriptionID;
120 private SOAPEvent previousEvent = null;
122 public static final String HELIOS_URI = "http://www.2n.cz/2013/event";
123 public static final String DIALECT_URI = "http://www.2n.cz/2013/TopicExpression/Multiple";
124 public static final String WSN_URI = "http://docs.oasis-open.org/wsn/b-2";
126 public static final QName TOPIC_EXPRESSION = new QName(WSN_URI, "TopicExpression");
127 public static final QName INITIAL_TERMINATION_TIME = new QName(WSN_URI, "InitialTerminationTime");
128 public static final QName MAXIMUM_NUMBER = new QName(HELIOS_URI, "MaximumNumber");
129 public static final QName SIMPLE_MESSAGES = new QName(HELIOS_URI, "SimpleMessages");
130 public static final QName START_TIME_STAMP = new QName(HELIOS_URI, "StartTimestamp");
131 public static final QName START_RECORD_ID = new QName(HELIOS_URI, "StartRecordId");
133 public HeliosHandler27(Thing thing) {
138 public void handleCommand(ChannelUID channelUID, Command command) {
139 if (!(command instanceof RefreshType)) {
140 // 2N.cz has not yet released the automation part of the Helios IP HTTP/SOAP based API. Only the
141 // notification part has been documented, so for now there is nothing to do
143 logger.debug("The Helios IP is a read-only device and can not handle commands");
147 public String getSubscriptionID() {
148 return subscriptionID;
151 @SuppressWarnings("rawtypes")
153 public void initialize() {
154 logger.debug("Initializing Helios IP handler.");
155 List<Handler> handlerChain = new ArrayList<>();
156 handlerChain.add(new SOAPActionHandler());
159 context = JAXBContext.newInstance(SOAPObjectFactory.class, SOAPEvent.class, SOAPKeyPressed.class,
160 SOAPCallStateChanged.class, SOAPCodeEntered.class, SOAPCardEntered.class, SOAPDeviceState.class);
161 } catch (JAXBException e) {
162 logger.error("An exception occurred while setting up the JAXB Context factory: {}", e.getMessage(), e);
163 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
164 throw new RuntimeException();
168 if (endpoint == null || (endpoint != null && !endpoint.isPublished())) {
169 String address = "http://" + (String) getConfig().get(OPENHAB_IP_ADDRESS) + ":"
170 + ((BigDecimal) getConfig().get(OPENHAB_PORT_NUMBER)).toString() + "/notification"
171 + System.currentTimeMillis();
172 logger.debug("Publishing the notification consumer webservice on '{}", address);
173 endpoint = Endpoint.publish(address, this);
174 ((javax.xml.ws.soap.SOAPBinding) endpoint.getBinding()).setHandlerChain(handlerChain);
176 } catch (WebServiceException e1) {
177 logger.debug("An exception occurred while setting up the notification consumer webservice: {}",
178 e1.getMessage(), e1);
179 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e1.getMessage());
184 String heliosAddress = "http://" + (String) getConfig().get(IP_ADDRESS) + "/notification";
185 Service notificationProducerService = Service.create(WSNWSDLLocator.getWSDLUrl(),
186 new QName("http://cxf.apache.org/wsn/jaxws", "NotificationProducerService"));
187 notificationProducer = notificationProducerService.getPort(
188 new W3CEndpointReferenceBuilder().address(heliosAddress).build(), NotificationProducer.class);
189 ((BindingProvider) notificationProducer).getBinding().setHandlerChain(handlerChain);
190 } catch (WebServiceException e1) {
191 logger.debug("An exception occurred while setting up the notification webservice client: {}",
192 e1.getMessage(), e1);
193 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e1.getMessage());
198 // set up the access to the subscription manager on the Helios IP
199 // Vario so that we can renew in the future
200 String heliosAddress = "http://" + (String) getConfig().get(IP_ADDRESS) + "/notification";
201 Service subscriptionService = Service.create(WSNWSDLLocator.getWSDLUrl(),
202 new QName("http://cxf.apache.org/wsn/jaxws", "PausableSubscriptionManagerService"));
203 subscription = subscriptionService.getPort(new W3CEndpointReferenceBuilder().address(heliosAddress).build(),
204 PausableSubscriptionManager.class);
206 handlerChain = new ArrayList<>();
207 handlerChain.add(new SOAPSubscriptionActionHandler(this));
208 ((BindingProvider) subscription).getBinding().setHandlerChain(handlerChain);
209 } catch (WebServiceException e1) {
210 logger.debug("An exception occurred while setting up the subscription manager client: {}", e1.getMessage(),
212 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e1.getMessage());
217 subscribe(endpoint.getEndpointReference(W3CEndpointReference.class));
218 } catch (WebServiceException | TopicNotSupportedFault | InvalidFilterFault | TopicExpressionDialectUnknownFault
219 | UnacceptableInitialTerminationTimeFault | SubscribeCreationFailedFault
220 | InvalidMessageContentExpressionFault | InvalidTopicExpressionFault | UnrecognizedPolicyRequestFault
221 | UnsupportedPolicyRequestFault | ResourceUnknownFault | NotifyMessageNotSupportedFault
222 | InvalidProducerPropertiesExpressionFault e) {
223 logger.debug("An exception occurred while subscribing to the notifications for thing '{}': {}",
224 getThing().getUID(), e.getMessage(), e);
225 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
229 updateStatus(ThingStatus.ONLINE);
233 public void dispose() {
234 logger.debug("Disposing Helios IP handler.");
238 if (endpoint != null) {
242 terminationTime = null;
244 notificationProducer = null;
246 } catch (Exception e) {
247 logger.error("An exception occurred while disposing the Helios Thing Handler : {}", e.getMessage(), e);
253 @WebParam(partName = "Notify", name = "Notify", targetNamespace = "http://docs.oasis-open.org/wsn/b-2") Notify notify) {
254 for (Object object : notify.getAny()) {
256 this.processNotification(context.createUnmarshaller().unmarshal((Element) object, SOAPEvent.class));
257 } catch (JAXBException e) {
258 logger.error("An exception occurred while processing a notification message : {}", e.getMessage(), e);
263 public void processNotification(JAXBElement<SOAPEvent> message) {
264 if (getThing().getStatus() == ThingStatus.ONLINE) {
265 SOAPEvent event = message.getValue();
266 // WS-Notification does not provide a mechanism to query existing
267 // subscriptions established before, so these keep lingering on the
268 // remote device. Therefore, when restarting the OH runtime, we
269 // might receive events more than one time, we need to filter these
271 if (previousEvent == null || !previousEvent.equals(event)) {
272 previousEvent = event;
274 Object data = event.getData();
276 if (data instanceof SOAPKeyPressed) {
277 StringType valueType = new StringType(((SOAPKeyPressed) data).getKeyCode());
278 updateState(new ChannelUID(getThing().getUID(), KEY_PRESSED), valueType);
280 DateTimeType stampType = new DateTimeType(event.getTimestamp());
281 updateState(new ChannelUID(getThing().getUID(), KEY_PRESSED_STAMP), stampType);
284 if (data instanceof SOAPCallStateChanged) {
285 StringType valueType = new StringType(((SOAPCallStateChanged) data).getState());
286 updateState(new ChannelUID(getThing().getUID(), CALL_STATE), valueType);
288 valueType = new StringType(((SOAPCallStateChanged) data).getDirection());
289 updateState(new ChannelUID(getThing().getUID(), CALL_DIRECTION), valueType);
291 DateTimeType stampType = new DateTimeType(event.getTimestamp());
292 updateState(new ChannelUID(getThing().getUID(), CALL_STATE_STAMP), stampType);
295 if (data instanceof SOAPCardEntered) {
296 StringType valueType = new StringType(((SOAPCardEntered) data).getCard());
297 updateState(new ChannelUID(getThing().getUID(), CARD), valueType);
299 valueType = new StringType(((SOAPCardEntered) data).getValid());
300 updateState(new ChannelUID(getThing().getUID(), CARD_VALID), valueType);
302 DateTimeType stampType = new DateTimeType(event.getTimestamp());
303 updateState(new ChannelUID(getThing().getUID(), CARD_STAMP), stampType);
306 if (data instanceof SOAPCodeEntered) {
307 StringType valueType = new StringType(((SOAPCodeEntered) data).getCode());
308 updateState(new ChannelUID(getThing().getUID(), CODE), valueType);
310 valueType = new StringType(((SOAPCodeEntered) data).getValid());
311 updateState(new ChannelUID(getThing().getUID(), CODE_VALID), valueType);
313 DateTimeType stampType = new DateTimeType(event.getTimestamp());
314 updateState(new ChannelUID(getThing().getUID(), CODE_STAMP), stampType);
317 if (data instanceof SOAPDeviceState) {
318 StringType valueType = new StringType(((SOAPDeviceState) data).getState());
319 updateState(new ChannelUID(getThing().getUID(), DEVICE_STATE), valueType);
321 DateTimeType stampType = new DateTimeType(event.getTimestamp());
322 updateState(new ChannelUID(getThing().getUID(), DEVICE_STATE_STAMP), stampType);
325 logger.warn("Duplicate event received due to lingering subscriptions: '{}':'{}'", event.getEventName(),
326 event.getTimestamp());
331 public void renew(String newTerminationTime) throws ResourceUnknownFault, UnacceptableTerminationTimeFault {
332 if (subscription != null) {
333 Renew renew = new Renew();
334 renew.setTerminationTime(newTerminationTime);
336 RenewResponse response = subscription.renew(renew);
337 currentTime = response.getCurrentTime().toGregorianCalendar();
338 terminationTime = response.getTerminationTime().toGregorianCalendar();
340 SimpleDateFormat pFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
341 logger.debug("Renewed the subscription with ID '{}' for '{}' from {} until {}",
342 new Object[] { getSubscriptionID(), getThing().getUID(), pFormatter.format(currentTime.getTime()),
343 pFormatter.format(terminationTime.getTime()) });
347 public void unsubscribe() {
348 if (subscription != null) {
350 UnsubscribeResponse response = subscription.unsubscribe(new Unsubscribe());
351 logger.debug("Unsubscribing the subscription with ID '{}' for '{}' ", getSubscriptionID(),
352 getThing().getUID());
353 } catch (UnableToDestroySubscriptionFault | ResourceUnknownFault e) {
354 logger.error("An exception occurred while unsubscribing from the subscription : {}", e.getMessage(), e);
359 public void subscribe(W3CEndpointReference epr)
360 throws TopicNotSupportedFault, InvalidFilterFault, TopicExpressionDialectUnknownFault,
361 UnacceptableInitialTerminationTimeFault, SubscribeCreationFailedFault, InvalidMessageContentExpressionFault,
362 InvalidTopicExpressionFault, UnrecognizedPolicyRequestFault, UnsupportedPolicyRequestFault,
363 ResourceUnknownFault, NotifyMessageNotSupportedFault, InvalidProducerPropertiesExpressionFault {
364 if (notificationProducer != null) {
365 Subscribe subscribeRequest = new Subscribe();
366 subscribeRequest.setConsumerReference(epr);
367 subscribeRequest.setFilter(new FilterType());
369 TopicExpressionType topicExp = new TopicExpressionType();
370 topicExp.getContent().add("");
371 topicExp.setDialect(DIALECT_URI);
372 subscribeRequest.getFilter().getAny()
373 .add(new JAXBElement<>(TOPIC_EXPRESSION, TopicExpressionType.class, topicExp));
375 subscribeRequest.setInitialTerminationTime(
376 new JAXBElement<>(INITIAL_TERMINATION_TIME, String.class, SUBSCRIPTION_PERIOD));
378 subscribeRequest.setSubscriptionPolicy(new Subscribe.SubscriptionPolicy());
379 subscribeRequest.getSubscriptionPolicy().getAny().add(new JAXBElement<>(MAXIMUM_NUMBER, Integer.class, 1));
380 subscribeRequest.getSubscriptionPolicy().getAny().add(new JAXBElement<>(SIMPLE_MESSAGES, Integer.class, 1));
381 GregorianCalendar now = new GregorianCalendar();
382 SimpleDateFormat pFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
383 subscribeRequest.getSubscriptionPolicy().getAny()
384 .add(new JAXBElement<>(START_TIME_STAMP, String.class, pFormatter.format(now.getTime())));
386 SubscribeResponse response = notificationProducer.subscribe(subscribeRequest);
388 if (response != null) {
389 currentTime = response.getCurrentTime().toGregorianCalendar();
390 terminationTime = response.getTerminationTime().toGregorianCalendar();
391 subscriptionReference = response.getSubscriptionReference();
393 Element element = DOMUtils.createDocument().createElement("elem");
394 subscriptionReference.writeTo(new DOMResult(element));
395 NodeList nl = element.getElementsByTagNameNS("http://www.2n.cz/2013/event", "SubscriptionId");
396 if (nl != null && nl.getLength() > 0) {
397 Element e = (Element) nl.item(0);
398 subscriptionID = DOMUtils.getContent(e).trim();
400 logger.debug("Established a subscription with ID '{}' for '{}' as from {} until {}",
401 new Object[] { subscriptionID, getThing().getUID(), pFormatter.format(currentTime.getTime()),
402 pFormatter.format(terminationTime.getTime()) });
404 java.util.Calendar triggerTime = terminationTime;
405 triggerTime.add(Calendar.MINUTE, -1);
407 logger.debug("Scheduling a renewal of the subscription with ID '{}' for '{}' to happen on {}",
408 new Object[] { subscriptionID, getThing().getUID(), pFormatter.format(triggerTime.getTime()) });
410 scheduler.schedule(renewRunnable, triggerTime.getTimeInMillis() - System.currentTimeMillis(),
411 TimeUnit.MILLISECONDS);
412 } catch (RejectedExecutionException e) {
413 logger.error("An exception occurred while scheduling a renewal : '{}'", e.getMessage(), e);
419 protected Runnable renewRunnable = new Runnable() {
423 SimpleDateFormat pFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
424 boolean result = false;
427 ((HeliosHandler27) getThing().getHandler()).renew(SUBSCRIPTION_PERIOD);
429 } catch (Exception e) {
430 logger.error("An exception occurred while renewing the subscription : {}", e.getMessage(), e);
431 ((HeliosHandler27) getThing().getHandler()).dispose();
432 ((HeliosHandler27) getThing().getHandler()).initialize();
437 java.util.Calendar triggerTime = terminationTime;
438 triggerTime.add(Calendar.MINUTE, -1);
440 logger.debug("Scheduling a renewal of the subscription with ID '{}' for '{}' to happen on {}",
441 new Object[] { subscriptionID, getThing().getUID(), pFormatter.format(triggerTime.getTime()) });
443 scheduler.schedule(renewRunnable, triggerTime.getTimeInMillis() - System.currentTimeMillis(),
444 TimeUnit.MILLISECONDS);
445 } catch (RejectedExecutionException e) {
446 logger.error("An exception occurred while scheduling a renewal : '{}'", e.getMessage(), e);