mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[ipcamera] Improve onvif events with an auto restart feature (#17518)
* Refactor and improve onvif events with auto restart. Signed-off-by: Matthew Skinner <matt@pcmus.com>
This commit is contained in:
parent
20b889c029
commit
989093c826
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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()) {
|
||||
|
Loading…
Reference in New Issue
Block a user