]> git.basschouten.com Git - openhab-addons.git/blob
94885257152eba2a52e06c5a1b416e748c380fed
[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.ipcamera.internal.onvif;
14
15 import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
16
17 import java.net.InetSocketAddress;
18 import java.nio.charset.StandardCharsets;
19 import java.security.MessageDigest;
20 import java.security.NoSuchAlgorithmException;
21 import java.security.SecureRandom;
22 import java.text.SimpleDateFormat;
23 import java.util.ArrayList;
24 import java.util.Base64;
25 import java.util.Date;
26 import java.util.LinkedList;
27 import java.util.List;
28 import java.util.Random;
29 import java.util.TimeZone;
30 import java.util.concurrent.Executors;
31 import java.util.concurrent.ScheduledExecutorService;
32 import java.util.concurrent.TimeUnit;
33 import java.util.concurrent.locks.ReentrantLock;
34
35 import org.eclipse.jdt.annotation.NonNullByDefault;
36 import org.eclipse.jdt.annotation.Nullable;
37 import org.openhab.binding.ipcamera.internal.Helper;
38 import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
39 import org.openhab.core.library.types.OnOffType;
40 import org.openhab.core.thing.ChannelUID;
41 import org.openhab.core.types.StateOption;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
45 import io.netty.bootstrap.Bootstrap;
46 import io.netty.buffer.ByteBuf;
47 import io.netty.buffer.Unpooled;
48 import io.netty.channel.Channel;
49 import io.netty.channel.ChannelFuture;
50 import io.netty.channel.ChannelFutureListener;
51 import io.netty.channel.ChannelInitializer;
52 import io.netty.channel.ChannelOption;
53 import io.netty.channel.EventLoopGroup;
54 import io.netty.channel.nio.NioEventLoopGroup;
55 import io.netty.channel.socket.SocketChannel;
56 import io.netty.channel.socket.nio.NioSocketChannel;
57 import io.netty.handler.codec.http.DefaultFullHttpRequest;
58 import io.netty.handler.codec.http.FullHttpRequest;
59 import io.netty.handler.codec.http.HttpClientCodec;
60 import io.netty.handler.codec.http.HttpHeaderValues;
61 import io.netty.handler.codec.http.HttpMethod;
62 import io.netty.handler.codec.http.HttpVersion;
63 import io.netty.handler.timeout.IdleStateHandler;
64
65 /**
66  * The {@link OnvifConnection} This is a basic Netty implementation for connecting and communicating to ONVIF cameras.
67  *
68  *
69  *
70  * @author Matthew Skinner - Initial contribution
71  */
72
73 @NonNullByDefault
74 public class OnvifConnection {
75     public static enum RequestType {
76         AbsoluteMove,
77         AddPTZConfiguration,
78         ContinuousMoveLeft,
79         ContinuousMoveRight,
80         ContinuousMoveUp,
81         ContinuousMoveDown,
82         Stop,
83         ContinuousMoveIn,
84         ContinuousMoveOut,
85         CreatePullPointSubscription,
86         GetCapabilities,
87         GetDeviceInformation,
88         GetProfiles,
89         GetServiceCapabilities,
90         GetSnapshotUri,
91         GetStreamUri,
92         GetSystemDateAndTime,
93         Subscribe,
94         Unsubscribe,
95         PullMessages,
96         GetEventProperties,
97         RelativeMoveLeft,
98         RelativeMoveRight,
99         RelativeMoveUp,
100         RelativeMoveDown,
101         RelativeMoveIn,
102         RelativeMoveOut,
103         Renew,
104         GetConfigurations,
105         GetConfigurationOptions,
106         GetConfiguration,
107         SetConfiguration,
108         GetNodes,
109         GetStatus,
110         GotoPreset,
111         GetPresets
112     }
113
114     private final Logger logger = LoggerFactory.getLogger(getClass());
115     private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(2);
116     private @Nullable Bootstrap bootstrap;
117     private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup(2);
118     private ReentrantLock connecting = new ReentrantLock();
119     private String ipAddress = "";
120     private String user = "";
121     private String password = "";
122     private int onvifPort = 80;
123     private String deviceXAddr = "http://" + ipAddress + "/onvif/device_service";
124     private String eventXAddr = "http://" + ipAddress + "/onvif/device_service";
125     private String mediaXAddr = "http://" + ipAddress + "/onvif/device_service";
126     @SuppressWarnings("unused")
127     private String imagingXAddr = "http://" + ipAddress + "/onvif/device_service";
128     private String ptzXAddr = "http://" + ipAddress + "/onvif/ptz_service";
129     private String subscriptionXAddr = "http://" + ipAddress + "/onvif/device_service";
130     private boolean isConnected = false;
131     private int mediaProfileIndex = 0;
132     private String snapshotUri = "";
133     private String rtspUri = "";
134     private IpCameraHandler ipCameraHandler;
135     private boolean usingEvents = false;
136
137     // These hold the cameras PTZ position in the range that the camera uses, ie
138     // mine is -1 to +1
139     private Float panRangeMin = -1.0f;
140     private Float panRangeMax = 1.0f;
141     private Float tiltRangeMin = -1.0f;
142     private Float tiltRangeMax = 1.0f;
143     private Float zoomMin = 0.0f;
144     private Float zoomMax = 1.0f;
145     // These hold the PTZ values for updating Openhabs controls in 0-100 range
146     private Float currentPanPercentage = 0.0f;
147     private Float currentTiltPercentage = 0.0f;
148     private Float currentZoomPercentage = 0.0f;
149     private Float currentPanCamValue = 0.0f;
150     private Float currentTiltCamValue = 0.0f;
151     private Float currentZoomCamValue = 0.0f;
152     private String ptzNodeToken = "000";
153     private String ptzConfigToken = "000";
154     private int presetTokenIndex = 0;
155     private List<String> presetTokens = new LinkedList<>();
156     private List<String> presetNames = new LinkedList<>();
157     private List<String> mediaProfileTokens = new LinkedList<>();
158     private boolean ptzDevice = true;
159
160     public OnvifConnection(IpCameraHandler ipCameraHandler, String ipAddress, String user, String password) {
161         this.ipCameraHandler = ipCameraHandler;
162         if (!ipAddress.isEmpty()) {
163             this.user = user;
164             this.password = password;
165             getIPandPortFromUrl(ipAddress);
166         }
167     }
168
169     private String getXml(RequestType requestType) {
170         try {
171             switch (requestType) {
172                 case AbsoluteMove:
173                     return "<AbsoluteMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
174                             + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken><Position><PanTilt x=\""
175                             + currentPanCamValue + "\" y=\"" + currentTiltCamValue
176                             + "\" space=\"http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace\">\n"
177                             + "</PanTilt>\n" + "<Zoom x=\"" + currentZoomCamValue
178                             + "\" space=\"http://www.onvif.org/ver10/tptz/ZoomSpaces/PositionGenericSpace\">\n"
179                             + "</Zoom>\n" + "</Position>\n"
180                             + "<Speed><PanTilt x=\"0.1\" y=\"0.1\" space=\"http://www.onvif.org/ver10/tptz/PanTiltSpaces/GenericSpeedSpace\"></PanTilt><Zoom x=\"1.0\" space=\"http://www.onvif.org/ver10/tptz/ZoomSpaces/ZoomGenericSpeedSpace\"></Zoom>\n"
181                             + "</Speed></AbsoluteMove>";
182                 case AddPTZConfiguration: // not tested to work yet
183                     return "<AddPTZConfiguration xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
184                             + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken><ConfigurationToken>"
185                             + ptzConfigToken + "</ConfigurationToken></AddPTZConfiguration>";
186                 case ContinuousMoveLeft:
187                     return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
188                             + mediaProfileTokens.get(mediaProfileIndex)
189                             + "</ProfileToken><Velocity><PanTilt x=\"-0.5\" y=\"0\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
190                 case ContinuousMoveRight:
191                     return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
192                             + mediaProfileTokens.get(mediaProfileIndex)
193                             + "</ProfileToken><Velocity><PanTilt x=\"0.5\" y=\"0\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
194                 case ContinuousMoveUp:
195                     return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
196                             + mediaProfileTokens.get(mediaProfileIndex)
197                             + "</ProfileToken><Velocity><PanTilt x=\"0\" y=\"-0.5\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
198                 case ContinuousMoveDown:
199                     return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
200                             + mediaProfileTokens.get(mediaProfileIndex)
201                             + "</ProfileToken><Velocity><PanTilt x=\"0\" y=\"0.5\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
202                 case Stop:
203                     return "<Stop xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
204                             + mediaProfileTokens.get(mediaProfileIndex)
205                             + "</ProfileToken><PanTilt>true</PanTilt><Zoom>true</Zoom></Stop>";
206                 case ContinuousMoveIn:
207                     return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
208                             + mediaProfileTokens.get(mediaProfileIndex)
209                             + "</ProfileToken><Velocity><Zoom x=\"0.5\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
210                 case ContinuousMoveOut:
211                     return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
212                             + mediaProfileTokens.get(mediaProfileIndex)
213                             + "</ProfileToken><Velocity><Zoom x=\"-0.5\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
214                 case CreatePullPointSubscription:
215                     return "<CreatePullPointSubscription xmlns=\"http://www.onvif.org/ver10/events/wsdl\"><InitialTerminationTime>PT600S</InitialTerminationTime></CreatePullPointSubscription>";
216                 case GetCapabilities:
217                     return "<GetCapabilities xmlns=\"http://www.onvif.org/ver10/device/wsdl\"><Category>All</Category></GetCapabilities>";
218
219                 case GetDeviceInformation:
220                     return "<GetDeviceInformation xmlns=\"http://www.onvif.org/ver10/device/wsdl\"/>";
221                 case GetProfiles:
222                     return "<GetProfiles xmlns=\"http://www.onvif.org/ver10/media/wsdl\"/>";
223                 case GetServiceCapabilities:
224                     return "<GetServiceCapabilities xmlns=\"http://docs.oasis-open.org/wsn/b-2/\"></GetServiceCapabilities>";
225                 case GetSnapshotUri:
226                     return "<GetSnapshotUri xmlns=\"http://www.onvif.org/ver10/media/wsdl\"><ProfileToken>"
227                             + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetSnapshotUri>";
228                 case GetStreamUri:
229                     return "<GetStreamUri xmlns=\"http://www.onvif.org/ver10/media/wsdl\"><StreamSetup><Stream xmlns=\"http://www.onvif.org/ver10/schema\">RTP-Unicast</Stream><Transport xmlns=\"http://www.onvif.org/ver10/schema\"><Protocol>RTSP</Protocol></Transport></StreamSetup><ProfileToken>"
230                             + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetStreamUri>";
231                 case GetSystemDateAndTime:
232                     return "<GetSystemDateAndTime xmlns=\"http://www.onvif.org/ver10/device/wsdl\"/>";
233                 case Subscribe:
234                     return "<Subscribe xmlns=\"http://docs.oasis-open.org/wsn/b-2/\"><ConsumerReference><Address>http://"
235                             + ipCameraHandler.hostIp + ":" + SERVLET_PORT + "/ipcamera/"
236                             + ipCameraHandler.getThing().getUID().getId()
237                             + "/OnvifEvent</Address></ConsumerReference></Subscribe>";
238                 case Unsubscribe:
239                     return "<Unsubscribe xmlns=\"http://docs.oasis-open.org/wsn/b-2/\"></Unsubscribe>";
240                 case PullMessages:
241                     return "<PullMessages xmlns=\"http://www.onvif.org/ver10/events/wsdl\"><Timeout>PT8S</Timeout><MessageLimit>1</MessageLimit></PullMessages>";
242                 case GetEventProperties:
243                     return "<GetEventProperties xmlns=\"http://www.onvif.org/ver10/events/wsdl\"/>";
244                 case RelativeMoveLeft:
245                     return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
246                             + mediaProfileTokens.get(mediaProfileIndex)
247                             + "</ProfileToken><Translation><PanTilt x=\"0.05000000\" y=\"0\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
248                 case RelativeMoveRight:
249                     return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
250                             + mediaProfileTokens.get(mediaProfileIndex)
251                             + "</ProfileToken><Translation><PanTilt x=\"-0.05000000\" y=\"0\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
252                 case RelativeMoveUp:
253                     return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
254                             + mediaProfileTokens.get(mediaProfileIndex)
255                             + "</ProfileToken><Translation><PanTilt x=\"0\" y=\"0.100000000\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
256                 case RelativeMoveDown:
257                     return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
258                             + mediaProfileTokens.get(mediaProfileIndex)
259                             + "</ProfileToken><Translation><PanTilt x=\"0\" y=\"-0.100000000\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
260                 case RelativeMoveIn:
261                     return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
262                             + mediaProfileTokens.get(mediaProfileIndex)
263                             + "</ProfileToken><Translation><Zoom x=\"0.0240506344\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
264                 case RelativeMoveOut:
265                     return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
266                             + mediaProfileTokens.get(mediaProfileIndex)
267                             + "</ProfileToken><Translation><Zoom x=\"-0.0240506344\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
268                 case Renew:
269                     return "<Renew xmlns=\"http://docs.oasis-open.org/wsn/b-2\"><TerminationTime>PT1M</TerminationTime></Renew>";
270                 case GetConfigurations:
271                     return "<GetConfigurations xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"></GetConfigurations>";
272                 case GetConfigurationOptions:
273                     return "<GetConfigurationOptions xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ConfigurationToken>"
274                             + ptzConfigToken + "</ConfigurationToken></GetConfigurationOptions>";
275                 case GetConfiguration:
276                     return "<GetConfiguration xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><PTZConfigurationToken>"
277                             + ptzConfigToken + "</PTZConfigurationToken></GetConfiguration>";
278                 case SetConfiguration:// not tested to work yet
279                     return "<SetConfiguration xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><PTZConfiguration><NodeToken>"
280                             + ptzNodeToken
281                             + "</NodeToken><DefaultAbsolutePantTiltPositionSpace>AbsolutePanTiltPositionSpace</DefaultAbsolutePantTiltPositionSpace><DefaultAbsoluteZoomPositionSpace>AbsoluteZoomPositionSpace</DefaultAbsoluteZoomPositionSpace></PTZConfiguration></SetConfiguration>";
282                 case GetNodes:
283                     return "<GetNodes xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"></GetNodes>";
284                 case GetStatus:
285                     return "<GetStatus xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
286                             + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetStatus>";
287                 case GotoPreset:
288                     return "<GotoPreset xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
289                             + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken><PresetToken>"
290                             + presetTokens.get(presetTokenIndex) + "</PresetToken></GotoPreset>";
291                 case GetPresets:
292                     return "<GetPresets xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
293                             + mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetPresets>";
294             }
295         } catch (IndexOutOfBoundsException e) {
296             if (!isConnected) {
297                 logger.debug("IndexOutOfBoundsException occured, camera is not connected via ONVIF: {}",
298                         e.getMessage());
299             } else {
300                 logger.debug("IndexOutOfBoundsException occured, {}", e.getMessage());
301             }
302         }
303         return "notfound";
304     }
305
306     public void processReply(String message) {
307         logger.trace("Onvif reply is:{}", message);
308         if (message.contains("PullMessagesResponse")) {
309             eventRecieved(message);
310         } else if (message.contains("RenewResponse")) {
311             sendOnvifRequest(RequestType.PullMessages, subscriptionXAddr);
312         } else if (message.contains("GetSystemDateAndTimeResponse")) {// 1st to be sent.
313             connecting.lock();
314             try {
315                 isConnected = true;
316             } finally {
317                 connecting.unlock();
318             }
319             parseDateAndTime(message);
320             logger.debug("Openhabs UTC dateTime is:{}", getUTCdateTime());
321         } else if (message.contains("GetCapabilitiesResponse")) {// 2nd to be sent.
322             parseXAddr(message);
323             sendOnvifRequest(RequestType.GetProfiles, mediaXAddr);
324         } else if (message.contains("GetProfilesResponse")) {// 3rd to be sent.
325             connecting.lock();
326             try {
327                 isConnected = true;
328             } finally {
329                 connecting.unlock();
330             }
331             parseProfiles(message);
332             sendOnvifRequest(RequestType.GetSnapshotUri, mediaXAddr);
333             sendOnvifRequest(RequestType.GetStreamUri, mediaXAddr);
334             if (ptzDevice) {
335                 sendPTZRequest(RequestType.GetNodes);
336             }
337             if (usingEvents) {// stops API cameras from getting sent ONVIF events.
338                 sendOnvifRequest(RequestType.GetEventProperties, eventXAddr);
339                 sendOnvifRequest(RequestType.GetServiceCapabilities, eventXAddr);
340             }
341         } else if (message.contains("GetServiceCapabilitiesResponse")) {
342             if (message.contains("WSSubscriptionPolicySupport=\"true\"")) {
343                 sendOnvifRequest(RequestType.Subscribe, eventXAddr);
344             }
345         } else if (message.contains("GetEventPropertiesResponse")) {
346             sendOnvifRequest(RequestType.CreatePullPointSubscription, eventXAddr);
347         } else if (message.contains("CreatePullPointSubscriptionResponse")) {
348             subscriptionXAddr = Helper.fetchXML(message, "SubscriptionReference>", "Address>");
349             logger.debug("subscriptionXAddr={}", subscriptionXAddr);
350             sendOnvifRequest(RequestType.PullMessages, subscriptionXAddr);
351         } else if (message.contains("GetStatusResponse")) {
352             processPTZLocation(message);
353         } else if (message.contains("GetPresetsResponse")) {
354             parsePresets(message);
355         } else if (message.contains("GetConfigurationsResponse")) {
356             sendPTZRequest(RequestType.GetPresets);
357             ptzConfigToken = Helper.fetchXML(message, "PTZConfiguration", "token=\"");
358             logger.debug("ptzConfigToken={}", ptzConfigToken);
359             sendPTZRequest(RequestType.GetConfigurationOptions);
360         } else if (message.contains("GetNodesResponse")) {
361             sendPTZRequest(RequestType.GetStatus);
362             ptzNodeToken = Helper.fetchXML(message, "", "token=\"");
363             logger.debug("ptzNodeToken={}", ptzNodeToken);
364             sendPTZRequest(RequestType.GetConfigurations);
365         } else if (message.contains("GetDeviceInformationResponse")) {
366             logger.debug("GetDeviceInformationResponse recieved");
367         } else if (message.contains("GetSnapshotUriResponse")) {
368             snapshotUri = removeIPfromUrl(Helper.fetchXML(message, ":MediaUri", ":Uri"));
369             logger.debug("GetSnapshotUri:{}", snapshotUri);
370             if (ipCameraHandler.snapshotUri.isEmpty()
371                     && !"ffmpeg".equals(ipCameraHandler.cameraConfig.getSnapshotUrl())) {
372                 ipCameraHandler.snapshotUri = snapshotUri;
373             }
374         } else if (message.contains("GetStreamUriResponse")) {
375             rtspUri = Helper.fetchXML(message, ":MediaUri", ":Uri>");
376             logger.debug("GetStreamUri:{}", rtspUri);
377             if (ipCameraHandler.cameraConfig.getFfmpegInput().isEmpty()) {
378                 ipCameraHandler.rtspUri = rtspUri;
379             }
380         }
381     }
382
383     /**
384      * The {@link removeIPfromUrl} Will throw away all text before the cameras IP, also removes the IP and the PORT
385      * leaving just the URL.
386      *
387      * @author Matthew Skinner - Initial contribution
388      */
389     String removeIPfromUrl(String url) {
390         int index = url.indexOf("//");
391         if (index != -1) {// now remove the :port
392             index = url.indexOf("/", index + 2);
393         }
394         if (index == -1) {
395             logger.debug("We hit an issue parsing url:{}", url);
396             return "";
397         }
398         return url.substring(index);
399     }
400
401     String extractIPportFromUrl(String url) {
402         int startIndex = url.indexOf("//") + 2;
403         int endIndex = url.indexOf("/", startIndex);// skip past any :port to the slash /
404         if (startIndex != -1 && endIndex != -1) {
405             return url.substring(startIndex, endIndex);
406         }
407         logger.debug("We hit an issue extracting IP:PORT from url:{}", url);
408         return "";
409     }
410
411     int extractPortFromUrl(String url) {
412         int startIndex = url.indexOf("//") + 2;// skip past http://
413         startIndex = url.indexOf(":", startIndex);
414         if (startIndex == -1) {// no port defined so use port 80
415             return 80;
416         }
417         int endIndex = url.indexOf("/", startIndex);// skip past any :port to the slash /
418         if (endIndex == -1) {
419             return 80;
420         }
421         return Integer.parseInt(url.substring(startIndex + 1, endIndex));
422     }
423
424     void parseXAddr(String message) {
425         // Normally I would search '<tt:XAddr>' instead but Foscam needed this work around.
426         String temp = Helper.fetchXML(message, "<tt:Device", "tt:XAddr");
427         if (!temp.isEmpty()) {
428             deviceXAddr = temp;
429             logger.debug("deviceXAddr:{}", deviceXAddr);
430         }
431         temp = Helper.fetchXML(message, "<tt:Events", "tt:XAddr");
432         if (!temp.isEmpty()) {
433             subscriptionXAddr = eventXAddr = temp;
434             logger.debug("eventsXAddr:{}", eventXAddr);
435         }
436         temp = Helper.fetchXML(message, "<tt:Media", "tt:XAddr");
437         if (!temp.isEmpty()) {
438             mediaXAddr = temp;
439             logger.debug("mediaXAddr:{}", mediaXAddr);
440         }
441
442         ptzXAddr = Helper.fetchXML(message, "<tt:PTZ", "tt:XAddr");
443         if (ptzXAddr.isEmpty()) {
444             ptzDevice = false;
445             logger.debug("Camera has no ONVIF PTZ support.");
446             List<org.openhab.core.thing.Channel> removeChannels = new ArrayList<>();
447             org.openhab.core.thing.Channel channel = ipCameraHandler.getThing().getChannel(CHANNEL_PAN);
448             if (channel != null) {
449                 removeChannels.add(channel);
450             }
451             channel = ipCameraHandler.getThing().getChannel(CHANNEL_TILT);
452             if (channel != null) {
453                 removeChannels.add(channel);
454             }
455             channel = ipCameraHandler.getThing().getChannel(CHANNEL_ZOOM);
456             if (channel != null) {
457                 removeChannels.add(channel);
458             }
459             ipCameraHandler.removeChannels(removeChannels);
460         } else {
461             logger.debug("ptzXAddr:{}", ptzXAddr);
462         }
463     }
464
465     private void parseDateAndTime(String message) {
466         String minute = Helper.fetchXML(message, "UTCDateTime", "Minute>");
467         String hour = Helper.fetchXML(message, "UTCDateTime", "Hour>");
468         String second = Helper.fetchXML(message, "UTCDateTime", "Second>");
469         String day = Helper.fetchXML(message, "UTCDateTime", "Day>");
470         String month = Helper.fetchXML(message, "UTCDateTime", "Month>");
471         String year = Helper.fetchXML(message, "UTCDateTime", "Year>");
472         logger.debug("Cameras  UTC dateTime is:{}-{}-{}T{}:{}:{}", year, month, day, hour, minute, second);
473     }
474
475     private String getUTCdateTime() {
476         SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
477         format.setTimeZone(TimeZone.getTimeZone("UTC"));
478         return format.format(new Date());
479     }
480
481     String createNonce() {
482         Random nonce = new SecureRandom();
483         return "" + nonce.nextInt();
484     }
485
486     String encodeBase64(String raw) {
487         return Base64.getEncoder().encodeToString(raw.getBytes());
488     }
489
490     String createDigest(String nOnce, String dateTime) {
491         String beforeEncryption = nOnce + dateTime + password;
492         MessageDigest msgDigest;
493         byte[] encryptedRaw = null;
494         try {
495             msgDigest = MessageDigest.getInstance("SHA-1");
496             msgDigest.reset();
497             msgDigest.update(beforeEncryption.getBytes(StandardCharsets.UTF_8));
498             encryptedRaw = msgDigest.digest();
499         } catch (NoSuchAlgorithmException e) {
500         }
501         return Base64.getEncoder().encodeToString(encryptedRaw);
502     }
503
504     public void sendOnvifRequest(RequestType requestType, String xAddr) {
505         logger.trace("Sending ONVIF request:{}", requestType);
506         String security = "";
507         String extraEnvelope = "";
508         String headerTo = "";
509         String getXmlCache = getXml(requestType);
510         if (requestType.equals(RequestType.CreatePullPointSubscription) || requestType.equals(RequestType.PullMessages)
511                 || requestType.equals(RequestType.Renew) || requestType.equals(RequestType.Unsubscribe)) {
512             headerTo = "<a:To s:mustUnderstand=\"1\">" + xAddr + "</a:To>";
513             extraEnvelope = " xmlns:a=\"http://www.w3.org/2005/08/addressing\"";
514         }
515         String headers;
516         if (!password.isEmpty() && !requestType.equals(RequestType.GetSystemDateAndTime)) {
517             String nonce = createNonce();
518             String dateTime = getUTCdateTime();
519             String digest = createDigest(nonce, dateTime);
520             security = "<Security s:mustUnderstand=\"1\" xmlns=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\"><UsernameToken><Username>"
521                     + user
522                     + "</Username><Password Type=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest\">"
523                     + digest
524                     + "</Password><Nonce EncodingType=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary\">"
525                     + encodeBase64(nonce)
526                     + "</Nonce><Created xmlns=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\">"
527                     + dateTime + "</Created></UsernameToken></Security>";
528             headers = "<s:Header>" + security + headerTo + "</s:Header>";
529         } else {// GetSystemDateAndTime must not be password protected as per spec.
530             headers = "";
531         }
532         FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("POST"),
533                 removeIPfromUrl(xAddr));
534         String actionString = Helper.fetchXML(getXmlCache, requestType.toString(), "xmlns=\"");
535         request.headers().add("Content-Type",
536                 "application/soap+xml; charset=utf-8; action=\"" + actionString + "/" + requestType + "\"");
537         request.headers().add("Charset", "utf-8");
538         request.headers().set("Host", ipAddress + ":" + onvifPort);
539         request.headers().set("Connection", HttpHeaderValues.CLOSE);
540         request.headers().set("Accept-Encoding", "gzip, deflate");
541         String fullXml = "<s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\"" + extraEnvelope + ">"
542                 + headers
543                 + "<s:Body xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">"
544                 + getXmlCache + "</s:Body></s:Envelope>";
545         request.headers().add("SOAPAction", "\"" + actionString + "/" + requestType + "\"");
546         ByteBuf bbuf = Unpooled.copiedBuffer(fullXml, StandardCharsets.UTF_8);
547         request.headers().set("Content-Length", bbuf.readableBytes());
548         request.content().clear().writeBytes(bbuf);
549
550         Bootstrap localBootstap = bootstrap;
551         if (localBootstap == null) {
552             mainEventLoopGroup = new NioEventLoopGroup(2);
553             localBootstap = new Bootstrap();
554             localBootstap.group(mainEventLoopGroup);
555             localBootstap.channel(NioSocketChannel.class);
556             localBootstap.option(ChannelOption.SO_KEEPALIVE, true);
557             localBootstap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
558             localBootstap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
559             localBootstap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
560             localBootstap.option(ChannelOption.TCP_NODELAY, true);
561             localBootstap.handler(new ChannelInitializer<SocketChannel>() {
562
563                 @Override
564                 public void initChannel(SocketChannel socketChannel) throws Exception {
565                     socketChannel.pipeline().addLast("idleStateHandler", new IdleStateHandler(0, 0, 70));
566                     socketChannel.pipeline().addLast("HttpClientCodec", new HttpClientCodec());
567                     socketChannel.pipeline().addLast("OnvifCodec", new OnvifCodec(getHandle()));
568                 }
569             });
570             bootstrap = localBootstap;
571         }
572         if (!mainEventLoopGroup.isShuttingDown()) {
573             localBootstap.connect(new InetSocketAddress(ipAddress, extractPortFromUrl(xAddr)))
574                     .addListener(new ChannelFutureListener() {
575
576                         @Override
577                         public void operationComplete(@Nullable ChannelFuture future) {
578                             if (future == null) {
579                                 return;
580                             }
581                             if (future.isSuccess()) {
582                                 Channel ch = future.channel();
583                                 ch.writeAndFlush(request);
584                             } else { // an error occured
585                                 logger.debug("Camera is not reachable when using xAddr:{}.", xAddr);
586                                 if (isConnected) {
587                                     disconnect();
588                                 }
589                             }
590                         }
591                     });
592         } else {
593             logger.debug("ONVIF message not sent as connection is shutting down");
594         }
595     }
596
597     OnvifConnection getHandle() {
598         return this;
599     }
600
601     void getIPandPortFromUrl(String url) {
602         int beginIndex = url.indexOf(":");
603         int endIndex = url.indexOf("/", beginIndex);
604         if (beginIndex >= 0 && endIndex == -1) {// 192.168.1.1:8080
605             ipAddress = url.substring(0, beginIndex);
606             onvifPort = Integer.parseInt(url.substring(beginIndex + 1));
607         } else if (beginIndex >= 0 && endIndex > beginIndex) {// 192.168.1.1:8080/foo/bar
608             ipAddress = url.substring(0, beginIndex);
609             onvifPort = Integer.parseInt(url.substring(beginIndex + 1, endIndex));
610         } else {// 192.168.1.1
611             ipAddress = url;
612             deviceXAddr = "http://" + ipAddress + "/onvif/device_service";
613             logger.debug("No Onvif Port found when parsing:{}", url);
614             return;
615         }
616         deviceXAddr = "http://" + ipAddress + ":" + onvifPort + "/onvif/device_service";
617     }
618
619     public void gotoPreset(int index) {
620         if (ptzDevice) {
621             if (index > 0) {// 0 is reserved for HOME as cameras seem to start at preset 1.
622                 if (presetTokens.isEmpty()) {
623                     logger.warn("Camera did not report any ONVIF preset locations, updating preset tokens now.");
624                     sendPTZRequest(RequestType.GetPresets);
625                 } else {
626                     presetTokenIndex = index - 1;
627                     sendPTZRequest(RequestType.GotoPreset);
628                 }
629             }
630         }
631     }
632
633     public void eventRecieved(String eventMessage) {
634         String topic = Helper.fetchXML(eventMessage, "Topic", "tns1:");
635         if (topic.isEmpty()) {
636             sendOnvifRequest(RequestType.Renew, subscriptionXAddr);
637             return;
638         }
639         String dataName = Helper.fetchXML(eventMessage, "tt:Data", "Name=\"");
640         String dataValue = Helper.fetchXML(eventMessage, "tt:Data", "Value=\"");
641         logger.debug("Onvif Event Topic:{}, Data:{}, Value:{}", topic, dataName, dataValue);
642         switch (topic) {
643             case "RuleEngine/CellMotionDetector/Motion":
644                 if ("true".equals(dataValue)) {
645                     ipCameraHandler.motionDetected(CHANNEL_CELL_MOTION_ALARM);
646                 } else if ("false".equals(dataValue)) {
647                     ipCameraHandler.noMotionDetected(CHANNEL_CELL_MOTION_ALARM);
648                 }
649                 break;
650             case "VideoSource/MotionAlarm":
651                 if ("true".equals(dataValue)) {
652                     ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
653                 } else if ("false".equals(dataValue)) {
654                     ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
655                 }
656                 break;
657             case "AudioAnalytics/Audio/DetectedSound":
658                 if ("true".equals(dataValue)) {
659                     ipCameraHandler.audioDetected();
660                 } else if ("false".equals(dataValue)) {
661                     ipCameraHandler.noAudioDetected();
662                 }
663                 break;
664             case "RuleEngine/FieldDetector/ObjectsInside":
665                 if ("true".equals(dataValue)) {
666                     ipCameraHandler.motionDetected(CHANNEL_FIELD_DETECTION_ALARM);
667                 } else if ("false".equals(dataValue)) {
668                     ipCameraHandler.noMotionDetected(CHANNEL_FIELD_DETECTION_ALARM);
669                 }
670                 break;
671             case "RuleEngine/LineDetector/Crossed":
672                 if ("ObjectId".equals(dataName)) {
673                     ipCameraHandler.motionDetected(CHANNEL_LINE_CROSSING_ALARM);
674                 } else {
675                     ipCameraHandler.noMotionDetected(CHANNEL_LINE_CROSSING_ALARM);
676                 }
677                 break;
678             case "RuleEngine/TamperDetector/Tamper":
679                 if ("true".equals(dataValue)) {
680                     ipCameraHandler.changeAlarmState(CHANNEL_TAMPER_ALARM, OnOffType.ON);
681                 } else if ("false".equals(dataValue)) {
682                     ipCameraHandler.changeAlarmState(CHANNEL_TAMPER_ALARM, OnOffType.OFF);
683                 }
684                 break;
685             case "Device/HardwareFailure/StorageFailure":
686                 if ("true".equals(dataValue)) {
687                     ipCameraHandler.changeAlarmState(CHANNEL_STORAGE_ALARM, OnOffType.ON);
688                 } else if ("false".equals(dataValue)) {
689                     ipCameraHandler.changeAlarmState(CHANNEL_STORAGE_ALARM, OnOffType.OFF);
690                 }
691                 break;
692             case "VideoSource/ImageTooDark/AnalyticsService":
693             case "VideoSource/ImageTooDark/ImagingService":
694             case "VideoSource/ImageTooDark/RecordingService":
695                 if ("true".equals(dataValue)) {
696                     ipCameraHandler.changeAlarmState(CHANNEL_TOO_DARK_ALARM, OnOffType.ON);
697                 } else if ("false".equals(dataValue)) {
698                     ipCameraHandler.changeAlarmState(CHANNEL_TOO_DARK_ALARM, OnOffType.OFF);
699                 }
700                 break;
701             case "VideoSource/GlobalSceneChange/AnalyticsService":
702             case "VideoSource/GlobalSceneChange/ImagingService":
703             case "VideoSource/GlobalSceneChange/RecordingService":
704                 if ("true".equals(dataValue)) {
705                     ipCameraHandler.changeAlarmState(CHANNEL_SCENE_CHANGE_ALARM, OnOffType.ON);
706                 } else if ("false".equals(dataValue)) {
707                     ipCameraHandler.changeAlarmState(CHANNEL_SCENE_CHANGE_ALARM, OnOffType.OFF);
708                 }
709                 break;
710             case "VideoSource/ImageTooBright/AnalyticsService":
711             case "VideoSource/ImageTooBright/ImagingService":
712             case "VideoSource/ImageTooBright/RecordingService":
713                 if ("true".equals(dataValue)) {
714                     ipCameraHandler.changeAlarmState(CHANNEL_TOO_BRIGHT_ALARM, OnOffType.ON);
715                 } else if ("false".equals(dataValue)) {
716                     ipCameraHandler.changeAlarmState(CHANNEL_TOO_BRIGHT_ALARM, OnOffType.OFF);
717                 }
718                 break;
719             case "VideoSource/ImageTooBlurry/AnalyticsService":
720             case "VideoSource/ImageTooBlurry/ImagingService":
721             case "VideoSource/ImageTooBlurry/RecordingService":
722                 if ("true".equals(dataValue)) {
723                     ipCameraHandler.changeAlarmState(CHANNEL_TOO_BLURRY_ALARM, OnOffType.ON);
724                 } else if ("false".equals(dataValue)) {
725                     ipCameraHandler.changeAlarmState(CHANNEL_TOO_BLURRY_ALARM, OnOffType.OFF);
726                 }
727                 break;
728             case "RuleEngine/MyRuleDetector/Visitor":
729                 if ("true".equals(dataValue)) {
730                     ipCameraHandler.changeAlarmState(CHANNEL_DOORBELL, OnOffType.ON);
731                 } else if ("false".equals(dataValue)) {
732                     ipCameraHandler.changeAlarmState(CHANNEL_DOORBELL, OnOffType.OFF);
733                 }
734                 break;
735             case "RuleEngine/MyRuleDetector/VehicleDetect":
736                 if ("true".equals(dataValue)) {
737                     ipCameraHandler.changeAlarmState(CHANNEL_CAR_ALARM, OnOffType.ON);
738                 } else if ("false".equals(dataValue)) {
739                     ipCameraHandler.changeAlarmState(CHANNEL_CAR_ALARM, OnOffType.OFF);
740                 }
741                 break;
742             case "RuleEngine/MyRuleDetector/DogCatDetect":
743                 if ("true".equals(dataValue)) {
744                     ipCameraHandler.changeAlarmState(CHANNEL_ANIMAL_ALARM, OnOffType.ON);
745                 } else if ("false".equals(dataValue)) {
746                     ipCameraHandler.changeAlarmState(CHANNEL_ANIMAL_ALARM, OnOffType.OFF);
747                 }
748                 break;
749             case "RuleEngine/MyRuleDetector/FaceDetect":
750                 if ("true".equals(dataValue)) {
751                     ipCameraHandler.changeAlarmState(CHANNEL_FACE_DETECTED, OnOffType.ON);
752                 } else if ("false".equals(dataValue)) {
753                     ipCameraHandler.changeAlarmState(CHANNEL_FACE_DETECTED, OnOffType.OFF);
754                 }
755                 break;
756             case "RuleEngine/MyRuleDetector/PeopleDetect":
757                 if ("true".equals(dataValue)) {
758                     ipCameraHandler.changeAlarmState(CHANNEL_HUMAN_ALARM, OnOffType.ON);
759                 } else if ("false".equals(dataValue)) {
760                     ipCameraHandler.changeAlarmState(CHANNEL_HUMAN_ALARM, OnOffType.OFF);
761                 }
762                 break;
763             default:
764                 logger.debug("Please report this camera has an un-implemented ONVIF event. Topic:{}", topic);
765         }
766         sendOnvifRequest(RequestType.Renew, subscriptionXAddr);
767     }
768
769     public boolean supportsPTZ() {
770         return ptzDevice;
771     }
772
773     public void getStatus() {
774         if (ptzDevice) {
775             sendPTZRequest(RequestType.GetStatus);
776         }
777     }
778
779     public Float getAbsolutePan() {
780         return currentPanPercentage;
781     }
782
783     public Float getAbsoluteTilt() {
784         return currentTiltPercentage;
785     }
786
787     public Float getAbsoluteZoom() {
788         return currentZoomPercentage;
789     }
790
791     public void setAbsolutePan(Float panValue) {// Value is 0-100% of cameras range
792         if (ptzDevice) {
793             currentPanPercentage = panValue;
794             currentPanCamValue = ((((panRangeMin - panRangeMax) * -1) / 100) * panValue + panRangeMin);
795         }
796     }
797
798     public void setAbsoluteTilt(Float tiltValue) {// Value is 0-100% of cameras range
799         if (ptzDevice) {
800             currentTiltPercentage = tiltValue;
801             currentTiltCamValue = ((((panRangeMin - panRangeMax) * -1) / 100) * tiltValue + tiltRangeMin);
802         }
803     }
804
805     public void setAbsoluteZoom(Float zoomValue) {// Value is 0-100% of cameras range
806         if (ptzDevice) {
807             currentZoomPercentage = zoomValue;
808             currentZoomCamValue = ((((zoomMin - zoomMax) * -1) / 100) * zoomValue + zoomMin);
809         }
810     }
811
812     public void absoluteMove() { // Camera wont move until PTZ values are set, then call this.
813         if (ptzDevice) {
814             sendPTZRequest(RequestType.AbsoluteMove);
815         }
816     }
817
818     public void setSelectedMediaProfile(int mediaProfileIndex) {
819         this.mediaProfileIndex = mediaProfileIndex;
820     }
821
822     List<String> listOfResults(String message, String heading, String key) {
823         List<String> results = new LinkedList<>();
824         String temp = "";
825         for (int startLookingFromIndex = 0; startLookingFromIndex != -1;) {
826             startLookingFromIndex = message.indexOf(heading, startLookingFromIndex);
827             if (startLookingFromIndex >= 0) {
828                 temp = Helper.fetchXML(message.substring(startLookingFromIndex), heading, key);
829                 if (!temp.isEmpty()) {
830                     logger.trace("String was found:{}", temp);
831                     results.add(temp);
832                 } else {
833                     return results;// key string must not exist so stop looking.
834                 }
835                 startLookingFromIndex += temp.length();
836             }
837         }
838         return results;
839     }
840
841     void parsePresets(String message) {
842         List<StateOption> presets = new ArrayList<>();
843         int counter = 1;// Presets start at 1 not 0. HOME may be added to index 0.
844         presetTokens = listOfResults(message, "<tptz:Preset", "token=\"");
845         presetNames = listOfResults(message, "<tptz:Preset", "<tt:Name>");
846         if (presetTokens.size() != presetNames.size()) {
847             logger.warn("Camera did not report the same number of Tokens and Names for PTZ presets");
848             return;
849         }
850         for (String value : presetNames) {
851             presets.add(new StateOption(Integer.toString(counter++), value));
852         }
853         ipCameraHandler.stateDescriptionProvider
854                 .setStateOptions(new ChannelUID(ipCameraHandler.getThing().getUID(), CHANNEL_GOTO_PRESET), presets);
855     }
856
857     void parseProfiles(String message) {
858         mediaProfileTokens = listOfResults(message, "<trt:Profiles", "token=\"");
859         if (mediaProfileIndex >= mediaProfileTokens.size()) {
860             logger.warn(
861                     "You have set the media profile to {} when the camera reported {} profiles. Falling back to mainstream 0.",
862                     mediaProfileIndex, mediaProfileTokens.size());
863             mediaProfileIndex = 0;
864         }
865     }
866
867     void processPTZLocation(String result) {
868         logger.debug("Processing new PTZ location now");
869
870         int beginIndex = result.indexOf("x=\"");
871         int endIndex = result.indexOf("\"", (beginIndex + 3));
872         if (beginIndex >= 0 && endIndex >= 0) {
873             currentPanCamValue = Float.parseFloat(result.substring(beginIndex + 3, endIndex));
874             currentPanPercentage = (((panRangeMin - currentPanCamValue) * -1) / ((panRangeMin - panRangeMax) * -1))
875                     * 100;
876             logger.debug("Pan is updating to:{} and the cam value is {}", Math.round(currentPanPercentage),
877                     currentPanCamValue);
878         } else {
879             logger.warn(
880                     "Binding could not determin the cameras current PTZ location. Not all cameras respond to GetStatus requests.");
881             return;
882         }
883
884         beginIndex = result.indexOf("y=\"");
885         endIndex = result.indexOf("\"", (beginIndex + 3));
886         if (beginIndex >= 0 && endIndex >= 0) {
887             currentTiltCamValue = Float.parseFloat(result.substring(beginIndex + 3, endIndex));
888             currentTiltPercentage = (((tiltRangeMin - currentTiltCamValue) * -1) / ((tiltRangeMin - tiltRangeMax) * -1))
889                     * 100;
890             logger.debug("Tilt is updating to:{} and the cam value is {}", Math.round(currentTiltPercentage),
891                     currentTiltCamValue);
892         } else {
893             return;
894         }
895
896         beginIndex = result.lastIndexOf("x=\"");
897         endIndex = result.indexOf("\"", (beginIndex + 3));
898         if (beginIndex >= 0 && endIndex >= 0) {
899             currentZoomCamValue = Float.parseFloat(result.substring(beginIndex + 3, endIndex));
900             currentZoomPercentage = (((zoomMin - currentZoomCamValue) * -1) / ((zoomMin - zoomMax) * -1)) * 100;
901             logger.debug("Zoom is updating to:{} and the cam value is {}", Math.round(currentZoomPercentage),
902                     currentZoomCamValue);
903         } else {
904             return;
905         }
906     }
907
908     public void sendPTZRequest(RequestType requestType) {
909         if (!isConnected) {
910             logger.debug("ONVIF was not connected when a PTZ request was made, connecting now");
911             connect(usingEvents);
912         }
913         sendOnvifRequest(requestType, ptzXAddr);
914     }
915
916     public void sendEventRequest(RequestType requestType) {
917         sendOnvifRequest(requestType, eventXAddr);
918     }
919
920     public void connect(boolean useEvents) {
921         connecting.lock();
922         try {
923             if (!isConnected) {
924                 logger.debug("Connecting {} to ONVIF", ipAddress);
925                 threadPool = Executors.newScheduledThreadPool(2);
926                 sendOnvifRequest(RequestType.GetSystemDateAndTime, deviceXAddr);
927                 usingEvents = useEvents;
928                 sendOnvifRequest(RequestType.GetCapabilities, deviceXAddr);
929             }
930         } finally {
931             connecting.unlock();
932         }
933     }
934
935     public boolean isConnected() {
936         connecting.lock();
937         try {
938             return isConnected;
939         } finally {
940             connecting.unlock();
941         }
942     }
943
944     private void cleanup() {
945         if (!isConnected && !mainEventLoopGroup.isShuttingDown()) {
946             try {
947                 mainEventLoopGroup.shutdownGracefully();
948                 mainEventLoopGroup.awaitTermination(3, TimeUnit.SECONDS);
949             } catch (InterruptedException e) {
950                 logger.warn("ONVIF was not cleanly shutdown, due to being interrupted");
951             } finally {
952                 logger.debug("Eventloop is shutdown:{}", mainEventLoopGroup.isShutdown());
953                 bootstrap = null;
954                 threadPool.shutdown();
955             }
956         }
957     }
958
959     public void disconnect() {
960         connecting.lock();// Lock out multiple disconnect()/connect() attempts as we try to send Unsubscribe.
961         try {
962             isConnected = false;// isConnected is not thread safe, connecting.lock() used as fix.
963             if (bootstrap != null) {
964                 if (usingEvents && !mainEventLoopGroup.isShuttingDown()) {
965                     // Some cameras may continue to send events even when they can't reach a server.
966                     sendOnvifRequest(RequestType.Unsubscribe, subscriptionXAddr);
967                 }
968                 // give time for the Unsubscribe request to be sent, shutdownGracefully will try to send it first.
969                 threadPool.schedule(this::cleanup, 50, TimeUnit.MILLISECONDS);
970             } else {
971                 cleanup();
972             }
973         } finally {
974             connecting.unlock();
975         }
976     }
977 }