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