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