Skip to content

Commit

Permalink
[ipcamera] Fix connection checks with ONVIF cameras with no snapshots (
Browse files Browse the repository at this point in the history
…openhab#15119)

* Added connection check via IdleStateHandler events for sent onvif requests.
Also checking connect errors and setting new states connectError or refusedError accordingly.
On connect, only one request is sent to have less parallel actions in case of reconnect, timeout.
Moved GetCapabilities call to GetSystemDateAndTime response handler part.
Added support to disable automatic polling at startup.

* Fixed call of sendOnvifRequest (missed to remove one call of requestBuilder).

---------

Signed-off-by: Thomas Burri <th@thonojato.ch>
Signed-off-by: Matthew Skinner <matt@pcmus.com>
Co-authored-by: Matthew Skinner <matt@pcmus.com>
  • Loading branch information
2 people authored and pat-git023 committed Oct 13, 2023
1 parent ef76e29 commit ff99fde
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,12 @@ private void checkCameraConnection() {
}
return;// ffmpeg snapshot stream is still alive
}

// if ONVIF cam also use connection state which is updated by regular messages to camera
if (thing.getThingTypeUID().getId().equals(ONVIF_THING) && snapshotUri.isEmpty() && onvifCamera.isConnected()) {
return;
}

// Open a HTTP connection without sending any requests as we do not need a snapshot.
Bootstrap localBootstrap = mainBootstrap;
if (localBootstrap != null) {
Expand Down Expand Up @@ -659,7 +665,7 @@ public void operationComplete(@Nullable ChannelFuture future) {
break;
}
ch.writeAndFlush(request);
} else { // an error occured
} else { // an error occurred
cameraCommunicationError(
"Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
}
Expand Down Expand Up @@ -1417,9 +1423,16 @@ void pollingCameraConnection() {
return;
}
if (cameraConfig.getOnvifPort() > 0 && !onvifCamera.isConnected()) {
if (onvifCamera.isConnectError()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Camera is not reachable");
} else if (onvifCamera.isRefusedError()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Camera refused connection on ONVIF ports.");
}
logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP:{}:{}", cameraConfig.getIp(),
cameraConfig.getOnvifPort());
onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING));
return;
}
if ("ffmpeg".equals(snapshotUri)) {
snapshotIsFfmpeg();
Expand Down Expand Up @@ -1556,9 +1569,6 @@ void pollCameraRunnable() {
if (!snapshotPolling) {
checkCameraConnection();
}
if (!onvifCamera.isConnected()) {
onvifCamera.connect(true);
}
break;
case INSTAR_THING:
if (!snapshotPolling) {
Expand Down Expand Up @@ -1758,15 +1768,26 @@ public void initialize() {
}

private void tryConnecting() {
int firstDelay = 4;
int normalDelay = 12; // doesn't make sense to have faster retry than CONNECT_TIMEOUT, which is 10 seconds, if
// camera is off
if (!thing.getThingTypeUID().getId().equals(GENERIC_THING)
&& !thing.getThingTypeUID().getId().equals(DOORBIRD_THING) && cameraConfig.getOnvifPort() > 0) {
onvifCamera = new OnvifConnection(this, cameraConfig.getIp() + ":" + cameraConfig.getOnvifPort(),
cameraConfig.getUser(), cameraConfig.getPassword());
onvifCamera.setSelectedMediaProfile(cameraConfig.getOnvifMediaProfile());
// Only use ONVIF events if it is not an API camera.
onvifCamera.connect(supportsOnvifEvents());

if (supportsOnvifEvents()) {
// it takes some time to try to retrieve the ONVIF snapshot and stream URLs and update internal members
// on first connect; if connection lost, doesn't make sense to poll to often
firstDelay = 12;
normalDelay = 30;
}
}
cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, 4, 8, TimeUnit.SECONDS);
cameraConnectionJob = threadPool.scheduleWithFixedDelay(this::pollingCameraConnection, firstDelay, normalDelay,
TimeUnit.SECONDS);
}

private boolean supportsOnvifEvents() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.CharsetUtil;
import io.netty.util.ReferenceCountUtil;

Expand Down Expand Up @@ -58,6 +59,21 @@ public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object ms
}
}

@Override
public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
if (ctx == null) {
return;
}
if (evt instanceof IdleStateEvent) {
IdleStateEvent e = (IdleStateEvent) evt;
logger.trace("IdleStateEvent received {}", e.state());
onvifConnection.setIsConnected(false);
ctx.close();
} else {
logger.trace("Other ONVIF netty channel event occured {}", evt);
}
}

@Override
public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
if (ctx == null || cause == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;

import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
Expand Down Expand Up @@ -50,6 +51,7 @@
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ConnectTimeoutException;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
Expand Down Expand Up @@ -127,6 +129,8 @@ public enum RequestType {
private String imagingXAddr = "http://" + ipAddress + "/onvif/device_service";
private String ptzXAddr = "http://" + ipAddress + "/onvif/ptz_service";
private String subscriptionXAddr = "http://" + ipAddress + "/onvif/device_service";
private boolean connectError = false;
private boolean refusedError = false;
private boolean isConnected = false;
private int mediaProfileIndex = 0;
private String snapshotUri = "";
Expand Down Expand Up @@ -310,24 +314,15 @@ public void processReply(String message) {
} else if (message.contains("RenewResponse")) {
sendOnvifRequest(RequestType.PullMessages, subscriptionXAddr);
} else if (message.contains("GetSystemDateAndTimeResponse")) {// 1st to be sent.
connecting.lock();
try {
isConnected = true;
} finally {
connecting.unlock();
}
setIsConnected(true);
sendOnvifRequest(RequestType.GetCapabilities, deviceXAddr);
parseDateAndTime(message);
logger.debug("Openhabs 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.
connecting.lock();
try {
isConnected = true;
} finally {
connecting.unlock();
}
setIsConnected(true);
parseProfiles(message);
sendOnvifRequest(RequestType.GetSnapshotUri, mediaXAddr);
sendOnvifRequest(RequestType.GetStreamUri, mediaXAddr);
Expand Down Expand Up @@ -562,33 +557,44 @@ public void sendOnvifRequest(RequestType requestType, String xAddr) {

@Override
public void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast("idleStateHandler", new IdleStateHandler(0, 0, 70));
socketChannel.pipeline().addLast("idleStateHandler", new IdleStateHandler(20, 20, 20));
socketChannel.pipeline().addLast("HttpClientCodec", new HttpClientCodec());
socketChannel.pipeline().addLast("OnvifCodec", new OnvifCodec(getHandle()));
}
});
bootstrap = localBootstap;
}
if (!mainEventLoopGroup.isShuttingDown()) {
localBootstap.connect(new InetSocketAddress(ipAddress, extractPortFromUrl(xAddr)))
.addListener(new ChannelFutureListener() {
bootstrap.connect(new InetSocketAddress(ipAddress, onvifPort)).addListener(new ChannelFutureListener() {

@Override
public void operationComplete(@Nullable ChannelFuture future) {
if (future == null) {
return;
}
if (future.isSuccess()) {
Channel ch = future.channel();
ch.writeAndFlush(request);
} else { // an error occured
logger.debug("Camera is not reachable when using xAddr:{}.", xAddr);
if (isConnected) {
disconnect();
}
@Override
public void operationComplete(@Nullable ChannelFuture future) {
if (future == null) {
return;
}
if (future.isSuccess()) {
connectError = false;
Channel ch = future.channel();
ch.writeAndFlush(request);
} else { // an error occured
if (future.isDone() && !future.isCancelled()) {
Throwable cause = future.cause();
logger.trace("connect failed - cause {}", cause.getMessage());
if (cause instanceof ConnectTimeoutException) {
logger.debug("Camera is not reachable on IP {}", ipAddress);
connectError = true;
} else if ((cause instanceof ConnectException)
&& cause.getMessage().contains("Connection refused")) {
logger.debug("Camera ONVIF port {} is refused.", onvifPort);
refusedError = true;
}
}
});
if (isConnected) {
disconnect();
}
}
}
});
} else {
logger.debug("ONVIF message not sent as connection is shutting down");
}
Expand Down Expand Up @@ -932,6 +938,14 @@ public void connect(boolean useEvents) {
}
}

public boolean isConnectError() {
return connectError;
}

public boolean isRefusedError() {
return refusedError;
}

public boolean isConnected() {
connecting.lock();
try {
Expand All @@ -941,6 +955,17 @@ public boolean isConnected() {
}
}

public void setIsConnected(boolean isConnected) {
connecting.lock();
try {
this.isConnected = isConnected;
this.connectError = false;
this.refusedError = false;
} finally {
connecting.unlock();
}
}

private void cleanup() {
if (!isConnected && !mainEventLoopGroup.isShuttingDown()) {
try {
Expand All @@ -959,9 +984,9 @@ private void cleanup() {
public void disconnect() {
connecting.lock();// Lock out multiple disconnect()/connect() attempts as we try to send Unsubscribe.
try {
isConnected = false;// isConnected is not thread safe, connecting.lock() used as fix.
if (bootstrap != null) {
if (usingEvents && !mainEventLoopGroup.isShuttingDown()) {
if (isConnected && usingEvents && !mainEventLoopGroup.isShuttingDown()) {
// Only makes sense to send if connected
// Some cameras may continue to send events even when they can't reach a server.
sendOnvifRequest(RequestType.Unsubscribe, subscriptionXAddr);
}
Expand All @@ -970,6 +995,8 @@ public void disconnect() {
} else {
cleanup();
}

isConnected = false;// isConnected is not thread safe, connecting.lock() used as fix.
} finally {
connecting.unlock();
}
Expand Down

0 comments on commit ff99fde

Please sign in to comment.