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