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.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;
93 * The {@link HeliosHandler27} is responsible for handling commands, which are
94 * sent to one of the channels.
96 * @author Karel Goderis - Initial contribution
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 {
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";
107 private final Logger logger = LoggerFactory.getLogger(HeliosHandler27.class);
109 private static final String SUBSCRIPTION_PERIOD = "PT1H";
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;
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";
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");
132 public HeliosHandler27(Thing thing) {
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
142 logger.debug("The Helios IP is a read-only device and can not handle commands");
146 public String getSubscriptionID() {
147 return subscriptionID;
150 @SuppressWarnings("rawtypes")
152 public void initialize() {
153 logger.debug("Initializing Helios IP handler.");
154 List<Handler> handlerChain = new ArrayList<>();
155 handlerChain.add(new SOAPActionHandler());
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();
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);
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());
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());
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);
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(),
211 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e1.getMessage());
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());
228 updateStatus(ThingStatus.ONLINE);
232 public void dispose() {
233 logger.debug("Disposing Helios IP handler.");
237 if (endpoint != null) {
241 terminationTime = null;
243 notificationProducer = null;
245 } catch (Exception e) {
246 logger.error("An exception occurred while disposing the Helios Thing Handler : {}", e.getMessage(), e);
252 @WebParam(partName = "Notify", name = "Notify", targetNamespace = "http://docs.oasis-open.org/wsn/b-2") Notify notify) {
253 for (Object object : notify.getAny()) {
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);
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
270 if (previousEvent == null || !previousEvent.equals(event)) {
271 previousEvent = event;
273 Object data = event.getData();
275 if (data instanceof SOAPKeyPressed) {
276 StringType valueType = new StringType(((SOAPKeyPressed) data).getKeyCode());
277 updateState(new ChannelUID(getThing().getUID(), KEY_PRESSED), valueType);
279 DateTimeType stampType = new DateTimeType(event.getTimestamp());
280 updateState(new ChannelUID(getThing().getUID(), KEY_PRESSED_STAMP), stampType);
283 if (data instanceof SOAPCallStateChanged) {
284 StringType valueType = new StringType(((SOAPCallStateChanged) data).getState());
285 updateState(new ChannelUID(getThing().getUID(), CALL_STATE), valueType);
287 valueType = new StringType(((SOAPCallStateChanged) data).getDirection());
288 updateState(new ChannelUID(getThing().getUID(), CALL_DIRECTION), valueType);
290 DateTimeType stampType = new DateTimeType(event.getTimestamp());
291 updateState(new ChannelUID(getThing().getUID(), CALL_STATE_STAMP), stampType);
294 if (data instanceof SOAPCardEntered) {
295 StringType valueType = new StringType(((SOAPCardEntered) data).getCard());
296 updateState(new ChannelUID(getThing().getUID(), CARD), valueType);
298 valueType = new StringType(((SOAPCardEntered) data).getValid());
299 updateState(new ChannelUID(getThing().getUID(), CARD_VALID), valueType);
301 DateTimeType stampType = new DateTimeType(event.getTimestamp());
302 updateState(new ChannelUID(getThing().getUID(), CARD_STAMP), stampType);
305 if (data instanceof SOAPCodeEntered) {
306 StringType valueType = new StringType(((SOAPCodeEntered) data).getCode());
307 updateState(new ChannelUID(getThing().getUID(), CODE), valueType);
309 valueType = new StringType(((SOAPCodeEntered) data).getValid());
310 updateState(new ChannelUID(getThing().getUID(), CODE_VALID), valueType);
312 DateTimeType stampType = new DateTimeType(event.getTimestamp());
313 updateState(new ChannelUID(getThing().getUID(), CODE_STAMP), stampType);
316 if (data instanceof SOAPDeviceState) {
317 StringType valueType = new StringType(((SOAPDeviceState) data).getState());
318 updateState(new ChannelUID(getThing().getUID(), DEVICE_STATE), valueType);
320 DateTimeType stampType = new DateTimeType(event.getTimestamp());
321 updateState(new ChannelUID(getThing().getUID(), DEVICE_STATE_STAMP), stampType);
324 logger.warn("Duplicate event received due to lingering subscriptions: '{}':'{}'", event.getEventName(),
325 event.getTimestamp());
330 public void renew(String newTerminationTime) throws ResourceUnknownFault, UnacceptableTerminationTimeFault {
331 if (subscription != null) {
332 Renew renew = new Renew();
333 renew.setTerminationTime(newTerminationTime);
335 RenewResponse response = subscription.renew(renew);
336 currentTime = response.getCurrentTime().toGregorianCalendar();
337 terminationTime = response.getTerminationTime().toGregorianCalendar();
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()) });
346 public void unsubscribe() {
347 if (subscription != null) {
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);
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());
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));
374 subscribeRequest.setInitialTerminationTime(
375 new JAXBElement<>(INITIAL_TERMINATION_TIME, String.class, SUBSCRIPTION_PERIOD));
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())));
385 SubscribeResponse response = notificationProducer.subscribe(subscribeRequest);
387 if (response != null) {
388 currentTime = response.getCurrentTime().toGregorianCalendar();
389 terminationTime = response.getTerminationTime().toGregorianCalendar();
390 subscriptionReference = response.getSubscriptionReference();
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();
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()) });
403 java.util.Calendar triggerTime = terminationTime;
404 triggerTime.add(Calendar.MINUTE, -1);
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()) });
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);
418 protected Runnable renewRunnable = new Runnable() {
422 SimpleDateFormat pFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
423 boolean result = false;
426 ((HeliosHandler27) getThing().getHandler()).renew(SUBSCRIPTION_PERIOD);
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();
436 java.util.Calendar triggerTime = terminationTime;
437 triggerTime.add(Calendar.MINUTE, -1);
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()) });
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);