]> git.basschouten.com Git - openhab-addons.git/commitdiff
[ipcamera] Improve onvif events with an auto restart feature (#17518)
authorMatthew Skinner <matt@pcmus.com>
Fri, 11 Oct 2024 07:48:46 +0000 (18:48 +1100)
committerGitHub <noreply@github.com>
Fri, 11 Oct 2024 07:48:46 +0000 (09:48 +0200)
* Refactor and improve onvif events with auto restart.

Signed-off-by: Matthew Skinner <matt@pcmus.com>
bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/IpCameraBindingConstants.java
bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/handler/IpCameraHandler.java
bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/onvif/OnvifCodec.java
bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/onvif/OnvifConnection.java

index ddbe231d1157222c5e97fe819bdec57f670ddd98..b6183d68ceb767cd8ee9aa0570586d87a6a155d5 100644 (file)
@@ -35,6 +35,7 @@ public class IpCameraBindingConstants {
     public static final String INSTAR_HANDLER = "instarHandler";
     public static final String REOLINK_HANDLER = "reolinkHandler";
     public static final String HIKVISION_HANDLER = "hikvisionHandler";
+    public static final String ONVIF_CODEC = "onvifCodec";
 
     public enum FFmpegFormat {
         HLS,
index 0004c569572166feb14d221814660dbe20307de4..94c9cb437c51c640490517e0ebec32b22b22bf48 100644 (file)
@@ -1561,6 +1561,11 @@ public class IpCameraHandler extends BaseThingHandler {
                 break;
             case ONVIF_THING:
                 onvifCamera.sendOnvifRequest(RequestType.Renew, onvifCamera.subscriptionXAddr);
+                if (onvifCamera.pullMessageRequests.intValue() == 0) {
+                    logger.info("The alarm stream was not running for ONVIF camera {}, re-starting it now",
+                            cameraConfig.getIp());
+                    onvifCamera.sendOnvifRequest(RequestType.PullMessages, onvifCamera.subscriptionXAddr);
+                }
                 break;
             case INSTAR_THING:
                 checkCameraConnection();
index 7c54ad978d1ef3b4999ea117d80187c3ebf7474d..f3e43c0fbf99909d33687eb994abf85f90419e5a 100644 (file)
@@ -14,6 +14,7 @@ package org.openhab.binding.ipcamera.internal.onvif;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.ipcamera.internal.onvif.OnvifConnection.RequestType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -36,6 +37,7 @@ public class OnvifCodec extends ChannelDuplexHandler {
     private final Logger logger = LoggerFactory.getLogger(getClass());
     private String incomingMessage = "";
     private OnvifConnection onvifConnection;
+    private RequestType requestType = RequestType.GetStatus;
 
     OnvifCodec(OnvifConnection onvifConnection) {
         this.onvifConnection = onvifConnection;
@@ -56,7 +58,7 @@ public class OnvifCodec extends ChannelDuplexHandler {
                 incomingMessage += content.content().toString(CharsetUtil.UTF_8);
             }
             if (msg instanceof LastHttpContent) {
-                onvifConnection.processReply(incomingMessage);
+                onvifConnection.processReply(requestType, incomingMessage);
                 ctx.close();
             }
         } finally {
@@ -87,4 +89,22 @@ public class OnvifCodec extends ChannelDuplexHandler {
         logger.debug("Exception on ONVIF connection: {}", cause.getMessage());
         ctx.close();
     }
+
+    @Override
+    public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
+        if (requestType == RequestType.PullMessages) {
+            onvifConnection.pullMessageRequests.decrementAndGet();
+        }
+    }
+
+    public void setRequestType(RequestType requestType) {
+        this.requestType = requestType;
+        if (requestType == RequestType.PullMessages) {
+            onvifConnection.pullMessageRequests.incrementAndGet();
+        }
+    }
+
+    public RequestType getRequestType() {
+        return requestType;
+    }
 }
index daab2b30d4afb30260295e2040beb170a1659393..71c0d7ac0a74fe29204ee8dec3e4c54bf9a3fa68 100644 (file)
@@ -20,6 +20,7 @@ import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
+import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Base64;
@@ -31,6 +32,7 @@ import java.util.TimeZone;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.locks.ReentrantLock;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -137,6 +139,7 @@ public class OnvifConnection {
     private boolean supportsEvents = false; // camera has replied that it can do events
     // Use/skip events even if camera support them. API cameras skip, as their own methods give better results.
     private boolean usingEvents = false;
+    public AtomicInteger pullMessageRequests = new AtomicInteger();
 
     // These hold the cameras PTZ position in the range that the camera uses, ie
     // mine is -1 to +1
@@ -307,87 +310,102 @@ public class OnvifConnection {
         return "notfound";
     }
 
-    public void processReply(String message) {
-        logger.trace("ONVIF reply is: {}", message);
-        if (message.contains("PullMessagesResponse")) {
-            eventRecieved(message);
-            sendOnvifRequest(RequestType.PullMessages, subscriptionXAddr);
-        } else if (message.contains("RenewResponse")) {
-        } else if (message.contains("GetSystemDateAndTimeResponse")) {// 1st to be sent.
-            setIsConnected(true);// Instar profile T only cameras need this
-            parseDateAndTime(message);
-            logger.debug("openHAB UTC dateTime is: {}", getUTCdateTime());
-        } else if (message.contains("GetCapabilitiesResponse")) {// 2nd to be sent.
-            parseXAddr(message);
-            sendOnvifRequest(RequestType.GetProfiles, mediaXAddr);
-        } else if (message.contains("GetProfilesResponse")) {// 3rd to be sent.
-            setIsConnected(true);
-            parseProfiles(message);
-            sendOnvifRequest(RequestType.GetSnapshotUri, mediaXAddr);
-            sendOnvifRequest(RequestType.GetStreamUri, mediaXAddr);
-            if (ptzDevice) {
-                sendPTZRequest(RequestType.GetNodes);
-            }
-            if (usingEvents) {// stops API cameras from getting sent ONVIF events.
-                sendOnvifRequest(RequestType.GetEventProperties, eventXAddr);
-                sendOnvifRequest(RequestType.GetServiceCapabilities, eventXAddr);
-            }
-        } else if (message.contains("GetServiceCapabilitiesResponse")) {
-            if (message.contains("WSSubscriptionPolicySupport=\"true\"")) {
-                sendOnvifRequest(RequestType.Subscribe, eventXAddr);
-            }
-        } else if (message.contains("GetEventPropertiesResponse")) {
-            sendOnvifRequest(RequestType.CreatePullPointSubscription, eventXAddr);
-        } else if (message.contains("CreatePullPointSubscriptionResponse")) {
-            supportsEvents = true;
-            subscriptionXAddr = Helper.fetchXML(message, "SubscriptionReference>", "Address>");
-            int start = message.indexOf("<dom0:SubscriptionId");
-            int end = message.indexOf("</dom0:SubscriptionId>");
-            if (start > -1 && end > start) {
-                subscriptionId = message.substring(start, end + 22);
-            }
-            logger.debug("subscriptionXAddr={} subscriptionId={}", subscriptionXAddr, subscriptionId);
-            sendOnvifRequest(RequestType.PullMessages, subscriptionXAddr);
-        } else if (message.contains("GetStatusResponse")) {
-            processPTZLocation(message);
-        } else if (message.contains("GetPresetsResponse")) {
-            parsePresets(message);
-        } else if (message.contains("GetConfigurationsResponse")) {
-            sendPTZRequest(RequestType.GetPresets);
-            ptzConfigToken = Helper.fetchXML(message, "PTZConfiguration", "token=\"");
-            logger.debug("ptzConfigToken={}", ptzConfigToken);
-            sendPTZRequest(RequestType.GetConfigurationOptions);
-        } else if (message.contains("GetNodesResponse")) {
-            sendPTZRequest(RequestType.GetStatus);
-            ptzNodeToken = Helper.fetchXML(message, "", "token=\"");
-            logger.debug("ptzNodeToken={}", ptzNodeToken);
-            sendPTZRequest(RequestType.GetConfigurations);
-        } else if (message.contains("GetDeviceInformationResponse")) {
-            logger.debug("GetDeviceInformationResponse received");
-        } else if (message.contains("GetSnapshotUriResponse")) {
-            String url = Helper.fetchXML(message, ":MediaUri", ":Uri");
-            if (!url.isBlank()) {
-                logger.debug("GetSnapshotUri: {}", url);
-                if (ipCameraHandler.snapshotUri.isEmpty()
-                        && !"ffmpeg".equals(ipCameraHandler.cameraConfig.getSnapshotUrl())) {
-                    ipCameraHandler.snapshotUri = ipCameraHandler.getCorrectUrlFormat(url);
-                    if (ipCameraHandler.getPortFromShortenedUrl(url) != ipCameraHandler.cameraConfig.getPort()) {
-                        logger.warn("ONVIF is reporting the snapshot does not match the things configured port of:{}",
-                                ipCameraHandler.cameraConfig.getPort());
+    public void processReply(RequestType requestType, String message) {
+        logger.trace("ONVIF {} reply is: {}", requestType, message);
+        switch (requestType) {
+            case CreatePullPointSubscription:
+                supportsEvents = true;
+                subscriptionXAddr = Helper.fetchXML(message, "SubscriptionReference>", "Address>");
+                int start = message.indexOf("<dom0:SubscriptionId");
+                int end = message.indexOf("</dom0:SubscriptionId>");
+                if (start > -1 && end > start) {
+                    subscriptionId = message.substring(start, end + 22);
+                }
+                logger.debug("subscriptionXAddr={} subscriptionId={}", subscriptionXAddr, subscriptionId);
+                sendOnvifRequest(RequestType.PullMessages, subscriptionXAddr);
+                break;
+            case GetCapabilities:
+                parseXAddr(message);
+                sendOnvifRequest(RequestType.GetProfiles, mediaXAddr);
+                break;
+            case GetDeviceInformation:
+                break;
+            case GetProfiles:
+                setIsConnected(true);
+                parseProfiles(message);
+                sendOnvifRequest(RequestType.GetSnapshotUri, mediaXAddr);
+                sendOnvifRequest(RequestType.GetStreamUri, mediaXAddr);
+                if (ptzDevice) {
+                    sendPTZRequest(RequestType.GetNodes);
+                }
+                if (usingEvents) {// stops API cameras from getting sent ONVIF events.
+                    sendOnvifRequest(RequestType.GetEventProperties, eventXAddr);
+                    sendOnvifRequest(RequestType.GetServiceCapabilities, eventXAddr);
+                }
+                break;
+            case GetServiceCapabilities:
+                if (message.contains("WSSubscriptionPolicySupport=\"true\"")) {
+                    sendOnvifRequest(RequestType.Subscribe, eventXAddr);
+                }
+                break;
+            case GetSnapshotUri:
+                String url = Helper.fetchXML(message, ":MediaUri", ":Uri");
+                if (!url.isBlank()) {
+                    logger.debug("GetSnapshotUri: {}", url);
+                    if (ipCameraHandler.snapshotUri.isEmpty()
+                            && !"ffmpeg".equals(ipCameraHandler.cameraConfig.getSnapshotUrl())) {
+                        ipCameraHandler.snapshotUri = ipCameraHandler.getCorrectUrlFormat(url);
+                        if (ipCameraHandler.getPortFromShortenedUrl(url) != ipCameraHandler.cameraConfig.getPort()) {
+                            logger.warn(
+                                    "ONVIF is reporting the snapshot does not match the things configured port of:{}",
+                                    ipCameraHandler.cameraConfig.getPort());
+                        }
                     }
                 }
-            }
-        } else if (message.contains("GetStreamUriResponse")) {
-            String xml = StringUtils.unEscapeXml(Helper.fetchXML(message, ":MediaUri", ":Uri>"));
-            if (xml != null) {
-                rtspUri = xml;
-                logger.debug("GetStreamUri: {}", rtspUri);
-                if (ipCameraHandler.cameraConfig.getFfmpegInput().isEmpty()) {
-                    ipCameraHandler.rtspUri = rtspUri;
+                break;
+            case GetStreamUri:
+                String xml = StringUtils.unEscapeXml(Helper.fetchXML(message, ":MediaUri", ":Uri>"));
+                if (xml != null) {
+                    rtspUri = xml;
+                    logger.debug("GetStreamUri: {}", rtspUri);
+                    if (ipCameraHandler.cameraConfig.getFfmpegInput().isEmpty()) {
+                        ipCameraHandler.rtspUri = rtspUri;
+                    }
                 }
-            }
-        } else {
-            logger.trace("Unhandled ONVIF reply is: {}", message);
+                break;
+            case GetSystemDateAndTime:
+                setIsConnected(true);// Instar profile T only cameras need this
+                parseDateAndTime(message);
+                break;
+            case PullMessages:
+                eventRecieved(message);
+                sendOnvifRequest(RequestType.PullMessages, subscriptionXAddr);
+                break;
+            case GetEventProperties:
+                sendOnvifRequest(RequestType.CreatePullPointSubscription, eventXAddr);
+                break;
+            case Renew:
+                break;
+            case GetConfiguration:
+                sendPTZRequest(RequestType.GetPresets);
+                ptzConfigToken = Helper.fetchXML(message, "PTZConfiguration", "token=\"");
+                logger.debug("ptzConfigToken={}", ptzConfigToken);
+                sendPTZRequest(RequestType.GetConfigurationOptions);
+                break;
+            case GetNodes:
+                sendPTZRequest(RequestType.GetStatus);
+                ptzNodeToken = Helper.fetchXML(message, "", "token=\"");
+                logger.debug("ptzNodeToken={}", ptzNodeToken);
+                sendPTZRequest(RequestType.GetConfigurations);
+                break;
+            case GetStatus:
+                processPTZLocation(message);
+                break;
+            case GetPresets:
+                parsePresets(message);
+                break;
+            default:
+                break;
         }
     }
 
@@ -475,13 +493,28 @@ public class OnvifConnection {
     }
 
     private void parseDateAndTime(String message) {
+        Date openHABTime = new Date();
         String minute = Helper.fetchXML(message, "UTCDateTime", "Minute>");
         String hour = Helper.fetchXML(message, "UTCDateTime", "Hour>");
         String second = Helper.fetchXML(message, "UTCDateTime", "Second>");
         String day = Helper.fetchXML(message, "UTCDateTime", "Day>");
         String month = Helper.fetchXML(message, "UTCDateTime", "Month>");
         String year = Helper.fetchXML(message, "UTCDateTime", "Year>");
-        logger.debug("Camera  UTC dateTime is: {}-{}-{}T{}:{}:{}", year, month, day, hour, minute, second);
+        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-M-d'T'H:m:s");
+        dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+        try {
+            String time = year + "-" + month + "-" + day + "T" + hour + ":" + minute + ":" + second;
+            Date cameraUTC = dateFormat.parse(time);
+            long timeOffset = cameraUTC.getTime() - openHABTime.getTime();
+            logger.debug("Camera  UTC dateTime is: {} openHAB time is {} time is offset by {}ms",
+                    dateFormat.format(cameraUTC.getTime()), dateFormat.format(openHABTime.getTime()), timeOffset);
+            if (timeOffset > 5000 || timeOffset < -5000) {
+                logger.warn(
+                        "ONVIF time in camera does not match openHAB's time, this can cause authentication issues as ONVIF requires the time to be close to each other");
+            }
+        } catch (ParseException e) {
+            logger.debug("Cameras time and date could not be parsed");
+        }
     }
 
     private String getUTCdateTime() {
@@ -583,7 +616,7 @@ public class OnvifConnection {
                 public void initChannel(SocketChannel socketChannel) throws Exception {
                     socketChannel.pipeline().addLast("idleStateHandler", new IdleStateHandler(0, 0, 18));
                     socketChannel.pipeline().addLast("HttpClientCodec", new HttpClientCodec());
-                    socketChannel.pipeline().addLast("OnvifCodec", new OnvifCodec(getHandle()));
+                    socketChannel.pipeline().addLast(ONVIF_CODEC, new OnvifCodec(getHandle()));
                 }
             });
             bootstrap = localBootstap;
@@ -591,6 +624,7 @@ public class OnvifConnection {
         if (!mainEventLoopGroup.isShuttingDown()) {
             // Tapo brand have different ports for the event xAddr to the other xAddr, can't use 1 port for all calls.
             localBootstap.connect(new InetSocketAddress(ipAddress, port)).addListener(new ChannelFutureListener() {
+
                 @Override
                 public void operationComplete(@Nullable ChannelFuture future) {
                     if (future == null) {
@@ -598,6 +632,8 @@ public class OnvifConnection {
                     }
                     if (future.isDone() && future.isSuccess()) {
                         Channel ch = future.channel();
+                        OnvifCodec onvifCodec = (OnvifCodec) ch.pipeline().get(ONVIF_CODEC);
+                        onvifCodec.setRequestType(requestType);
                         ch.writeAndFlush(request);
                     } else { // an error occurred
                         if (future.isDone() && !future.isCancelled()) {