added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.ipcamera-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
<feature name="openhab-binding-ipcamera" description="ipcamera Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<feature dependency="true">openhab.tp-jaxb</feature>
|
||||
<feature dependency="true">openhab.tp-netty</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.ipcamera/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
|
||||
package org.openhab.binding.ipcamera.internal;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
/**
|
||||
* The {@link AmcrestHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class AmcrestHandler extends ChannelDuplexHandler {
|
||||
private String requestUrl = "Empty";
|
||||
private IpCameraHandler ipCameraHandler;
|
||||
|
||||
public AmcrestHandler(ThingHandler handler) {
|
||||
ipCameraHandler = (IpCameraHandler) handler;
|
||||
}
|
||||
|
||||
public void setURL(String url) {
|
||||
requestUrl = url;
|
||||
}
|
||||
|
||||
// This handles the incoming http replies back from the camera.
|
||||
@Override
|
||||
public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
|
||||
if (msg == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String content = msg.toString();
|
||||
|
||||
if (!content.isEmpty()) {
|
||||
ipCameraHandler.logger.trace("HTTP Result back from camera is \t:{}:", content);
|
||||
}
|
||||
if (content.contains("Error: No Events")) {
|
||||
if ("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion".equals(requestUrl)) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
|
||||
} else if ("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation".equals(requestUrl)) {
|
||||
ipCameraHandler.noAudioDetected();
|
||||
}
|
||||
} else if (content.contains("channels[0]=0")) {
|
||||
if ("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=VideoMotion".equals(requestUrl)) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
|
||||
} else if ("/cgi-bin/eventManager.cgi?action=getEventIndexes&code=AudioMutation".equals(requestUrl)) {
|
||||
ipCameraHandler.audioDetected();
|
||||
}
|
||||
}
|
||||
|
||||
if (content.contains("table.MotionDetect[0].Enable=false")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.OFF);
|
||||
} else if (content.contains("table.MotionDetect[0].Enable=true")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.ON);
|
||||
}
|
||||
// determine if the audio alarm is turned on or off.
|
||||
if (content.contains("table.AudioDetect[0].MutationDetect=true")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.ON);
|
||||
} else if (content.contains("table.AudioDetect[0].MutationDetect=false")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.OFF);
|
||||
}
|
||||
// Handle AudioMutationThreshold alarm
|
||||
if (content.contains("table.AudioDetect[0].MutationThreold=")) {
|
||||
String value = ipCameraHandler.returnValueFromString(content, "table.AudioDetect[0].MutationThreold=");
|
||||
ipCameraHandler.setChannelState(CHANNEL_THRESHOLD_AUDIO_ALARM, PercentType.valueOf(value));
|
||||
}
|
||||
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
|
||||
// This handles the commands that come from the Openhab event bus.
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_THRESHOLD_AUDIO_ALARM:
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=getConfig&name=AudioDetect[0]");
|
||||
return;
|
||||
case CHANNEL_ENABLE_AUDIO_ALARM:
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=getConfig&name=AudioDetect[0]");
|
||||
return;
|
||||
case CHANNEL_ENABLE_LINE_CROSSING_ALARM:
|
||||
ipCameraHandler
|
||||
.sendHttpGET("/cgi-bin/configManager.cgi?action=getConfig&name=CrossLineDetection[0]");
|
||||
return;
|
||||
case CHANNEL_ENABLE_MOTION_ALARM:
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=getConfig&name=MotionDetect[0]");
|
||||
return;
|
||||
}
|
||||
return; // Return as we have handled the refresh command above and don't need to
|
||||
// continue further.
|
||||
} // end of "REFRESH"
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_TEXT_OVERLAY:
|
||||
String text = Helper.encodeSpecialChars(command.toString());
|
||||
if (text.isEmpty()) {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&VideoWidget[0].CustomTitle[1].EncodeBlend=false");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&VideoWidget[0].CustomTitle[1].EncodeBlend=true&VideoWidget[0].CustomTitle[1].Text="
|
||||
+ text);
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_LED:
|
||||
ipCameraHandler.setChannelState(CHANNEL_AUTO_LED, OnOffType.OFF);
|
||||
if (DecimalType.ZERO.equals(command) || OnOffType.OFF.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&Lighting[0][0].Mode=Off");
|
||||
} else if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler
|
||||
.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&Lighting[0][0].Mode=Manual");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&Lighting[0][0].Mode=Manual&Lighting[0][0].MiddleLight[0].Light="
|
||||
+ command.toString());
|
||||
}
|
||||
return;
|
||||
case CHANNEL_AUTO_LED:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_LED, UnDefType.UNDEF);
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&Lighting[0][0].Mode=Auto");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_THRESHOLD_AUDIO_ALARM:
|
||||
int threshold = Math.round(Float.valueOf(command.toString()));
|
||||
|
||||
if (threshold == 0) {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&AudioDetect[0].MutationThreold=1");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&AudioDetect[0].MutationThreold=" + threshold);
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_AUDIO_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&AudioDetect[0].MutationDetect=true&AudioDetect[0].EventHandler.Dejitter=1");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&AudioDetect[0].MutationDetect=false");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_LINE_CROSSING_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&VideoAnalyseRule[0][1].Enable=true");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&VideoAnalyseRule[0][1].Enable=false");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_MOTION_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&MotionDetect[0].Enable=true&MotionDetect[0].EventHandler.Dejitter=1");
|
||||
} else {
|
||||
ipCameraHandler
|
||||
.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&MotionDetect[0].Enable=false");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ACTIVATE_ALARM_OUTPUT:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&AlarmOut[0].Mode=1");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&AlarmOut[0].Mode=0");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ACTIVATE_ALARM_OUTPUT2:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&AlarmOut[1].Mode=1");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&AlarmOut[1].Mode=0");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_FFMPEG_MOTION_CONTROL:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.motionAlarmEnabled = true;
|
||||
} else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
|
||||
ipCameraHandler.motionAlarmEnabled = false;
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
|
||||
} else {
|
||||
ipCameraHandler.motionAlarmEnabled = true;
|
||||
ipCameraHandler.motionThreshold = Double.valueOf(command.toString());
|
||||
ipCameraHandler.motionThreshold = ipCameraHandler.motionThreshold / 10000;
|
||||
}
|
||||
ipCameraHandler.setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If a camera does not need to poll a request as often as snapshots, it can be
|
||||
// added here. Binding steps through the list.
|
||||
public ArrayList<String> getLowPriorityRequests() {
|
||||
ArrayList<String> lowPriorityRequests = new ArrayList<String>(1);
|
||||
return lowPriorityRequests;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.ipcamera.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link CameraConfig} handles the configuration of cameras.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class CameraConfig {
|
||||
private String ipAddress = "";
|
||||
private String ffmpegInputOptions = "";
|
||||
private int port;
|
||||
private int onvifPort;
|
||||
private int serverPort;
|
||||
private String username = "";
|
||||
private String password = "";
|
||||
private int onvifMediaProfile;
|
||||
private int pollTime;
|
||||
private String ffmpegInput = "";
|
||||
private String snapshotUrl = "";
|
||||
private String mjpegUrl = "";
|
||||
private String alarmInputUrl = "";
|
||||
private String customMotionAlarmUrl = "";
|
||||
private String customAudioAlarmUrl = "";
|
||||
private String updateImageWhen = "";
|
||||
private int nvrChannel;
|
||||
private String ipWhitelist = "";
|
||||
private String ffmpegLocation = "";
|
||||
private String ffmpegOutput = "";
|
||||
private String hlsOutOptions = "";
|
||||
private String gifOutOptions = "";
|
||||
private String mp4OutOptions = "";
|
||||
private String mjpegOptions = "";
|
||||
private String motionOptions = "";
|
||||
private boolean ptzContinuous;
|
||||
private int gifPreroll;
|
||||
|
||||
public int getOnvifMediaProfile() {
|
||||
return onvifMediaProfile;
|
||||
}
|
||||
|
||||
public String getFfmpegInputOptions() {
|
||||
return ffmpegInputOptions;
|
||||
}
|
||||
|
||||
public String getMjpegOptions() {
|
||||
return mjpegOptions;
|
||||
}
|
||||
|
||||
public String getMotionOptions() {
|
||||
return motionOptions;
|
||||
}
|
||||
|
||||
public String getMp4OutOptions() {
|
||||
return mp4OutOptions;
|
||||
}
|
||||
|
||||
public String getGifOutOptions() {
|
||||
return gifOutOptions;
|
||||
}
|
||||
|
||||
public String getHlsOutOptions() {
|
||||
return hlsOutOptions;
|
||||
}
|
||||
|
||||
public String getIpWhitelist() {
|
||||
return ipWhitelist;
|
||||
}
|
||||
|
||||
public String getFfmpegLocation() {
|
||||
return ffmpegLocation;
|
||||
}
|
||||
|
||||
public String getFfmpegOutput() {
|
||||
return ffmpegOutput;
|
||||
}
|
||||
|
||||
public boolean getPtzContinuous() {
|
||||
return ptzContinuous;
|
||||
}
|
||||
|
||||
public String getAlarmInputUrl() {
|
||||
return alarmInputUrl;
|
||||
}
|
||||
|
||||
public String getCustomAudioAlarmUrl() {
|
||||
return customAudioAlarmUrl;
|
||||
}
|
||||
|
||||
public String getCustomMotionAlarmUrl() {
|
||||
return customMotionAlarmUrl;
|
||||
}
|
||||
|
||||
public int getNvrChannel() {
|
||||
return nvrChannel;
|
||||
}
|
||||
|
||||
public String getMjpegUrl() {
|
||||
return mjpegUrl;
|
||||
}
|
||||
|
||||
public String getSnapshotUrl() {
|
||||
return snapshotUrl;
|
||||
}
|
||||
|
||||
public String getFfmpegInput() {
|
||||
return ffmpegInput;
|
||||
}
|
||||
|
||||
public String getUpdateImageWhen() {
|
||||
return updateImageWhen;
|
||||
}
|
||||
|
||||
public int getPollTime() {
|
||||
return pollTime;
|
||||
}
|
||||
|
||||
public int getOnvifPort() {
|
||||
return onvifPort;
|
||||
}
|
||||
|
||||
public int getServerPort() {
|
||||
return serverPort;
|
||||
}
|
||||
|
||||
public String getIp() {
|
||||
return ipAddress;
|
||||
}
|
||||
|
||||
public String getUser() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUser(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public int getGifPreroll() {
|
||||
return gifPreroll;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
|
||||
package org.openhab.binding.ipcamera.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
|
||||
/**
|
||||
* The {@link ChannelTracking} Can be used to find the handle for a HTTP channel if you know the URL. The reply can
|
||||
* optionally be stored for later use.
|
||||
*
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class ChannelTracking {
|
||||
private String storedReply = "";
|
||||
private String requestUrl = "";
|
||||
private Channel channel;
|
||||
|
||||
public ChannelTracking(Channel channel, String requestUrl) {
|
||||
this.channel = channel;
|
||||
this.requestUrl = requestUrl;
|
||||
}
|
||||
|
||||
public String getRequestUrl() {
|
||||
return requestUrl;
|
||||
}
|
||||
|
||||
public Channel getChannel() {
|
||||
return channel;
|
||||
}
|
||||
|
||||
public String getReply() {
|
||||
return storedReply;
|
||||
}
|
||||
|
||||
public void setReply(String replyToStore) {
|
||||
storedReply = replyToStore;
|
||||
}
|
||||
|
||||
public void setChannel(Channel ch) {
|
||||
channel = ch;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
|
||||
package org.openhab.binding.ipcamera.internal;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
/**
|
||||
* The {@link DahuaHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class DahuaHandler extends ChannelDuplexHandler {
|
||||
private IpCameraHandler ipCameraHandler;
|
||||
private int nvrChannel;
|
||||
|
||||
public DahuaHandler(IpCameraHandler handler, int nvrChannel) {
|
||||
ipCameraHandler = handler;
|
||||
this.nvrChannel = nvrChannel;
|
||||
}
|
||||
|
||||
// This handles the incoming http replies back from the camera.
|
||||
@Override
|
||||
public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
|
||||
if (msg == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
String content = msg.toString();
|
||||
try {
|
||||
if (!content.isEmpty()) {
|
||||
ipCameraHandler.logger.trace("HTTP Result back from camera is \t:{}:", content);
|
||||
}
|
||||
// determine if the motion detection is turned on or off.
|
||||
if (content.contains("table.MotionDetect[0].Enable=true")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.ON);
|
||||
} else if (content.contains("table.MotionDetect[" + nvrChannel + "].Enable=false")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.OFF);
|
||||
}
|
||||
// Handle motion alarm
|
||||
if (content.contains("Code=VideoMotion;action=Start;index=0")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
|
||||
} else if (content.contains("Code=VideoMotion;action=Stop;index=0")) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
|
||||
}
|
||||
// Handle item taken alarm
|
||||
if (content.contains("Code=TakenAwayDetection;action=Start;index=0")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_ITEM_TAKEN);
|
||||
} else if (content.contains("Code=TakenAwayDetection;action=Stop;index=0")) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_ITEM_TAKEN);
|
||||
}
|
||||
// Handle item left alarm
|
||||
if (content.contains("Code=LeftDetection;action=Start;index=0")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_ITEM_LEFT);
|
||||
} else if (content.contains("Code=LeftDetection;action=Stop;index=0")) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_ITEM_LEFT);
|
||||
}
|
||||
// Handle CrossLineDetection alarm
|
||||
if (content.contains("Code=CrossLineDetection;action=Start;index=0")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_LINE_CROSSING_ALARM);
|
||||
} else if (content.contains("Code=CrossLineDetection;action=Stop;index=0")) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_LINE_CROSSING_ALARM);
|
||||
}
|
||||
// determine if the audio alarm is turned on or off.
|
||||
if (content.contains("table.AudioDetect[0].MutationDetect=true")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.ON);
|
||||
} else if (content.contains("table.AudioDetect[0].MutationDetect=false")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.OFF);
|
||||
}
|
||||
// Handle AudioMutation alarm
|
||||
if (content.contains("Code=AudioMutation;action=Start;index=0")) {
|
||||
ipCameraHandler.audioDetected();
|
||||
} else if (content.contains("Code=AudioMutation;action=Stop;index=0")) {
|
||||
ipCameraHandler.noAudioDetected();
|
||||
}
|
||||
// Handle AudioMutationThreshold alarm
|
||||
if (content.contains("table.AudioDetect[0].MutationThreold=")) {
|
||||
String value = ipCameraHandler.returnValueFromString(content, "table.AudioDetect[0].MutationThreold=");
|
||||
ipCameraHandler.setChannelState(CHANNEL_THRESHOLD_AUDIO_ALARM, PercentType.valueOf(value));
|
||||
}
|
||||
// Handle FaceDetection alarm
|
||||
if (content.contains("Code=FaceDetection;action=Start;index=0")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_FACE_DETECTED);
|
||||
} else if (content.contains("Code=FaceDetection;action=Stop;index=0")) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_FACE_DETECTED);
|
||||
}
|
||||
// Handle ParkingDetection alarm
|
||||
if (content.contains("Code=ParkingDetection;action=Start;index=0")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_PARKING_ALARM);
|
||||
} else if (content.contains("Code=ParkingDetection;action=Stop;index=0")) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_PARKING_ALARM);
|
||||
}
|
||||
// Handle CrossRegionDetection alarm
|
||||
if (content.contains("Code=CrossRegionDetection;action=Start;index=0")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_FIELD_DETECTION_ALARM);
|
||||
} else if (content.contains("Code=CrossRegionDetection;action=Stop;index=0")) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_FIELD_DETECTION_ALARM);
|
||||
}
|
||||
// Handle External Input alarm
|
||||
if (content.contains("Code=AlarmLocal;action=Start;index=0")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_EXTERNAL_ALARM_INPUT, OnOffType.ON);
|
||||
} else if (content.contains("Code=AlarmLocal;action=Stop;index=0")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_EXTERNAL_ALARM_INPUT, OnOffType.OFF);
|
||||
}
|
||||
// Handle External Input alarm2
|
||||
if (content.contains("Code=AlarmLocal;action=Start;index=1")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_EXTERNAL_ALARM_INPUT2, OnOffType.ON);
|
||||
} else if (content.contains("Code=AlarmLocal;action=Stop;index=1")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_EXTERNAL_ALARM_INPUT2, OnOffType.OFF);
|
||||
}
|
||||
// CrossLineDetection alarm on/off
|
||||
if (content.contains("table.VideoAnalyseRule[0][1].Enable=true")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_LINE_CROSSING_ALARM, OnOffType.ON);
|
||||
} else if (content.contains("table.VideoAnalyseRule[0][1].Enable=false")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_LINE_CROSSING_ALARM, OnOffType.OFF);
|
||||
}
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// This handles the commands that come from the Openhab event bus.
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_THRESHOLD_AUDIO_ALARM:
|
||||
// ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=getConfig&name=AudioDetect[0]");
|
||||
return;
|
||||
case CHANNEL_ENABLE_AUDIO_ALARM:
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=getConfig&name=AudioDetect[0]");
|
||||
return;
|
||||
case CHANNEL_ENABLE_LINE_CROSSING_ALARM:
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=getConfig&name=VideoAnalyseRule");
|
||||
return;
|
||||
case CHANNEL_ENABLE_MOTION_ALARM:
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=getConfig&name=MotionDetect[0]");
|
||||
return;
|
||||
}
|
||||
return; // Return as we have handled the refresh command above and don't need to
|
||||
// continue further.
|
||||
} // end of "REFRESH"
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_TEXT_OVERLAY:
|
||||
String text = Helper.encodeSpecialChars(command.toString());
|
||||
if (text.isEmpty()) {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&VideoWidget[0].CustomTitle[1].EncodeBlend=false");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&VideoWidget[0].CustomTitle[1].EncodeBlend=true&VideoWidget[0].CustomTitle[1].Text="
|
||||
+ text);
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_LED:
|
||||
ipCameraHandler.setChannelState(CHANNEL_AUTO_LED, OnOffType.OFF);
|
||||
if (DecimalType.ZERO.equals(command) || OnOffType.OFF.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&Lighting[0][0].Mode=Off");
|
||||
} else if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler
|
||||
.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&Lighting[0][0].Mode=Manual");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&Lighting[0][0].Mode=Manual&Lighting[0][0].MiddleLight[0].Light="
|
||||
+ command.toString());
|
||||
}
|
||||
return;
|
||||
case CHANNEL_AUTO_LED:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_LED, UnDefType.UNDEF);
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&Lighting[0][0].Mode=Auto");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_THRESHOLD_AUDIO_ALARM:
|
||||
int threshold = Math.round(Float.valueOf(command.toString()));
|
||||
|
||||
if (threshold == 0) {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&AudioDetect[0].MutationThreold=1");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&AudioDetect[0].MutationThreold=" + threshold);
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_AUDIO_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&AudioDetect[0].MutationDetect=true&AudioDetect[0].EventHandler.Dejitter=1");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&AudioDetect[0].MutationDetect=false");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_LINE_CROSSING_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&VideoAnalyseRule[0][1].Enable=true");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&VideoAnalyseRule[0][1].Enable=false");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_MOTION_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/configManager.cgi?action=setConfig&MotionDetect[0].Enable=true&MotionDetect[0].EventHandler.Dejitter=1");
|
||||
} else {
|
||||
ipCameraHandler
|
||||
.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&MotionDetect[0].Enable=false");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ACTIVATE_ALARM_OUTPUT:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&AlarmOut[0].Mode=1");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&AlarmOut[0].Mode=0");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ACTIVATE_ALARM_OUTPUT2:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&AlarmOut[1].Mode=1");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/configManager.cgi?action=setConfig&AlarmOut[1].Mode=0");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_FFMPEG_MOTION_CONTROL:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.motionAlarmEnabled = true;
|
||||
} else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
|
||||
ipCameraHandler.motionAlarmEnabled = false;
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
|
||||
} else {
|
||||
ipCameraHandler.motionAlarmEnabled = true;
|
||||
ipCameraHandler.motionThreshold = Double.valueOf(command.toString()) / 10000;
|
||||
}
|
||||
ipCameraHandler.setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If a camera does not need to poll a request as often as snapshots, it can be
|
||||
// added here. Binding steps through the list.
|
||||
public ArrayList<String> getLowPriorityRequests() {
|
||||
ArrayList<String> lowPriorityRequests = new ArrayList<String>(1);
|
||||
return lowPriorityRequests;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
|
||||
package org.openhab.binding.ipcamera.internal;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
/**
|
||||
* The {@link DoorBirdHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class DoorBirdHandler extends ChannelDuplexHandler {
|
||||
private IpCameraHandler ipCameraHandler;
|
||||
|
||||
public DoorBirdHandler(ThingHandler handler) {
|
||||
ipCameraHandler = (IpCameraHandler) handler;
|
||||
}
|
||||
|
||||
// This handles the incoming http replies back from the camera.
|
||||
@Override
|
||||
public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
|
||||
if (msg == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
String content = msg.toString();
|
||||
try {
|
||||
if (!content.isEmpty()) {
|
||||
ipCameraHandler.logger.trace("HTTP Result back from camera is \t:{}:", content);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (content.contains("doorbell:H")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_DOORBELL, OnOffType.ON);
|
||||
}
|
||||
if (content.contains("doorbell:L")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_DOORBELL, OnOffType.OFF);
|
||||
}
|
||||
if (content.contains("motionsensor:L")) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
|
||||
}
|
||||
if (content.contains("motionsensor:H")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
|
||||
}
|
||||
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// This handles the commands that come from the Openhab event bus.
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
return;
|
||||
} // end of "REFRESH"
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_ACTIVATE_ALARM_OUTPUT:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET("/bha-api/open-door.cgi");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ACTIVATE_ALARM_OUTPUT2:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET("/bha-api/open-door.cgi?r=2");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_EXTERNAL_LIGHT:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET("/bha-api/light-on.cgi");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_FFMPEG_MOTION_CONTROL:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.motionAlarmEnabled = true;
|
||||
} else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
|
||||
ipCameraHandler.motionAlarmEnabled = false;
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
|
||||
} else {
|
||||
ipCameraHandler.motionAlarmEnabled = true;
|
||||
ipCameraHandler.motionThreshold = Double.valueOf(command.toString());
|
||||
ipCameraHandler.motionThreshold = ipCameraHandler.motionThreshold / 10000;
|
||||
}
|
||||
ipCameraHandler.setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If a camera does not need to poll a request as often as snapshots, it can be
|
||||
// added here. Binding steps through the list.
|
||||
public ArrayList<String> getLowPriorityRequests() {
|
||||
ArrayList<String> lowPriorityRequests = new ArrayList<String>(1);
|
||||
return lowPriorityRequests;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
|
||||
package org.openhab.binding.ipcamera.internal;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link Ffmpeg} class is responsible for handling multiple ffmpeg conversions which are used for many tasks
|
||||
*
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class Ffmpeg {
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private IpCameraHandler ipCameraHandler;
|
||||
private @Nullable Process process = null;
|
||||
private String ffmpegCommand = "";
|
||||
private FFmpegFormat format;
|
||||
private List<String> commandArrayList = new ArrayList<String>();
|
||||
private IpCameraFfmpegThread ipCameraFfmpegThread = new IpCameraFfmpegThread();
|
||||
private int keepAlive = 8;
|
||||
private boolean running = false;
|
||||
|
||||
public Ffmpeg(IpCameraHandler handle, FFmpegFormat format, String ffmpegLocation, String inputArguments,
|
||||
String input, String outArguments, String output, String username, String password) {
|
||||
this.format = format;
|
||||
ipCameraHandler = handle;
|
||||
String altInput = input;
|
||||
// Input can be snapshots not just rtsp or http
|
||||
if (!password.isEmpty() && !input.contains("@") && input.contains("rtsp")) {
|
||||
String credentials = username + ":" + password + "@";
|
||||
// will not work for https: but currently binding does not use https
|
||||
altInput = input.substring(0, 7) + credentials + input.substring(7);
|
||||
}
|
||||
if (inputArguments.isEmpty()) {
|
||||
ffmpegCommand = "-i " + altInput + " " + outArguments + " " + output;
|
||||
} else {
|
||||
ffmpegCommand = inputArguments + " -i " + altInput + " " + outArguments + " " + output;
|
||||
}
|
||||
Collections.addAll(commandArrayList, ffmpegCommand.trim().split("\\s+"));
|
||||
// ffmpegLocation may have a space in its folder
|
||||
commandArrayList.add(0, ffmpegLocation);
|
||||
}
|
||||
|
||||
public void setKeepAlive(int seconds) {
|
||||
if (seconds == -1) {
|
||||
keepAlive = -1;
|
||||
} else {// We now poll every 8 seconds due to mjpeg stream requirement.
|
||||
keepAlive = 8; // 64 seconds approx.
|
||||
}
|
||||
}
|
||||
|
||||
public void checkKeepAlive() {
|
||||
if (keepAlive <= -1) {
|
||||
return;
|
||||
} else if (keepAlive == 0) {
|
||||
stopConverting();
|
||||
} else {
|
||||
keepAlive--;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
private class IpCameraFfmpegThread extends Thread {
|
||||
private ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(2);
|
||||
public int countOfMotions;
|
||||
|
||||
IpCameraFfmpegThread() {
|
||||
setDaemon(true);
|
||||
}
|
||||
|
||||
private void gifCreated() {
|
||||
// Without a small delay, Pushover sends no file 10% of time.
|
||||
ipCameraHandler.setChannelState(CHANNEL_RECORDING_GIF, DecimalType.ZERO);
|
||||
ipCameraHandler.setChannelState(CHANNEL_GIF_HISTORY_LENGTH,
|
||||
new DecimalType(++ipCameraHandler.gifHistoryLength));
|
||||
}
|
||||
|
||||
private void mp4Created() {
|
||||
ipCameraHandler.setChannelState(CHANNEL_RECORDING_MP4, DecimalType.ZERO);
|
||||
ipCameraHandler.setChannelState(CHANNEL_MP4_HISTORY_LENGTH,
|
||||
new DecimalType(++ipCameraHandler.mp4HistoryLength));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
process = Runtime.getRuntime().exec(commandArrayList.toArray(new String[commandArrayList.size()]));
|
||||
if (process != null) {
|
||||
InputStream errorStream = process.getErrorStream();
|
||||
InputStreamReader errorStreamReader = new InputStreamReader(errorStream);
|
||||
BufferedReader bufferedReader = new BufferedReader(errorStreamReader);
|
||||
String line = null;
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
if (format.equals(FFmpegFormat.RTSP_ALARMS)) {
|
||||
logger.debug("{}", line);
|
||||
if (line.contains("lavfi.")) {
|
||||
if (countOfMotions == 4) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
|
||||
} else {
|
||||
countOfMotions++;
|
||||
}
|
||||
} else if (line.contains("speed=")) {
|
||||
if (countOfMotions > 0) {
|
||||
countOfMotions--;
|
||||
countOfMotions--;
|
||||
if (countOfMotions <= 0) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_FFMPEG_MOTION_ALARM);
|
||||
}
|
||||
}
|
||||
} else if (line.contains("silence_start")) {
|
||||
ipCameraHandler.noAudioDetected();
|
||||
} else if (line.contains("silence_end")) {
|
||||
ipCameraHandler.audioDetected();
|
||||
}
|
||||
} else {
|
||||
logger.debug("{}", line);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("An error occured trying to process the messages from FFmpeg.");
|
||||
} finally {
|
||||
switch (format) {
|
||||
case GIF:
|
||||
threadPool.schedule(this::gifCreated, 800, TimeUnit.MILLISECONDS);
|
||||
break;
|
||||
case RECORD:
|
||||
threadPool.schedule(this::mp4Created, 800, TimeUnit.MILLISECONDS);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void startConverting() {
|
||||
if (!ipCameraFfmpegThread.isAlive()) {
|
||||
ipCameraFfmpegThread = new IpCameraFfmpegThread();
|
||||
logger.debug("Starting ffmpeg with this command now:{}", ffmpegCommand);
|
||||
ipCameraFfmpegThread.start();
|
||||
running = true;
|
||||
if (format.equals(FFmpegFormat.HLS)) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_START_STREAM, OnOffType.ON);
|
||||
}
|
||||
}
|
||||
if (keepAlive != -1) {
|
||||
keepAlive = 8;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean getIsAlive() {
|
||||
return running;
|
||||
}
|
||||
|
||||
public void stopConverting() {
|
||||
if (ipCameraFfmpegThread.isAlive()) {
|
||||
logger.debug("Stopping ffmpeg {} now", format);
|
||||
running = false;
|
||||
if (process != null) {
|
||||
process.destroyForcibly();
|
||||
}
|
||||
if (format.equals(FFmpegFormat.HLS)) {
|
||||
if (keepAlive == -1) {
|
||||
logger.warn("HLS stopped when Stream should be running non stop, restarting HLS now.");
|
||||
startConverting();
|
||||
return;
|
||||
} else {
|
||||
ipCameraHandler.setChannelState(CHANNEL_START_STREAM, OnOffType.OFF);
|
||||
}
|
||||
}
|
||||
keepAlive = 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
|
||||
package org.openhab.binding.ipcamera.internal;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
/**
|
||||
* The {@link FoscamHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class FoscamHandler extends ChannelDuplexHandler {
|
||||
private IpCameraHandler ipCameraHandler;
|
||||
private String password, username;
|
||||
|
||||
public FoscamHandler(ThingHandler handler, String username, String password) {
|
||||
ipCameraHandler = (IpCameraHandler) handler;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
// This handles the incoming http replies back from the camera.
|
||||
@Override
|
||||
public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
|
||||
if (msg == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
String content = msg.toString();
|
||||
try {
|
||||
if (!content.isEmpty()) {
|
||||
ipCameraHandler.logger.trace("HTTP Result back from camera is \t:{}:", content);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
////////////// Motion Alarm //////////////
|
||||
if (content.contains("<motionDetectAlarm>")) {
|
||||
if (content.contains("<motionDetectAlarm>0</motionDetectAlarm>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.OFF);
|
||||
} else if (content.contains("<motionDetectAlarm>1</motionDetectAlarm>")) { // Enabled but no alarm
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.ON);
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
|
||||
} else if (content.contains("<motionDetectAlarm>2</motionDetectAlarm>")) {// Enabled, alarm on
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.ON);
|
||||
ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
|
||||
}
|
||||
}
|
||||
|
||||
////////////// Sound Alarm //////////////
|
||||
if (content.contains("<soundAlarm>0</soundAlarm>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.OFF);
|
||||
ipCameraHandler.setChannelState(CHANNEL_AUDIO_ALARM, OnOffType.OFF);
|
||||
}
|
||||
if (content.contains("<soundAlarm>1</soundAlarm>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.ON);
|
||||
ipCameraHandler.noAudioDetected();
|
||||
}
|
||||
if (content.contains("<soundAlarm>2</soundAlarm>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.ON);
|
||||
ipCameraHandler.audioDetected();
|
||||
}
|
||||
|
||||
////////////// Sound Threshold //////////////
|
||||
if (content.contains("<sensitivity>0</sensitivity>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_THRESHOLD_AUDIO_ALARM, PercentType.ZERO);
|
||||
}
|
||||
if (content.contains("<sensitivity>1</sensitivity>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_THRESHOLD_AUDIO_ALARM, PercentType.valueOf("50"));
|
||||
}
|
||||
if (content.contains("<sensitivity>2</sensitivity>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_THRESHOLD_AUDIO_ALARM, PercentType.HUNDRED);
|
||||
}
|
||||
|
||||
//////////////// Infrared LED /////////////////////
|
||||
if (content.contains("<infraLedState>0</infraLedState>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_LED, OnOffType.OFF);
|
||||
}
|
||||
if (content.contains("<infraLedState>1</infraLedState>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_LED, OnOffType.ON);
|
||||
}
|
||||
|
||||
if (content.contains("</CGI_Result>")) {
|
||||
ctx.close();
|
||||
ipCameraHandler.logger.debug("End of FOSCAM handler reached, so closing the channel to the camera now");
|
||||
}
|
||||
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// This handles the commands that come from the Openhab event bus.
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_THRESHOLD_AUDIO_ALARM:
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/CGIProxy.fcgi?cmd=getAudioAlarmConfig&usr=" + username + "&pwd=" + password);
|
||||
return;
|
||||
case CHANNEL_ENABLE_AUDIO_ALARM:
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/CGIProxy.fcgi?cmd=getAudioAlarmConfig&usr=" + username + "&pwd=" + password);
|
||||
return;
|
||||
case CHANNEL_ENABLE_MOTION_ALARM:
|
||||
ipCameraHandler
|
||||
.sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + username + "&pwd=" + password);
|
||||
return;
|
||||
}
|
||||
return; // Return as we have handled the refresh command above and don't need to
|
||||
// continue further.
|
||||
} // end of "REFRESH"
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_ENABLE_LED:
|
||||
// Disable the auto mode first
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/CGIProxy.fcgi?cmd=setInfraLedConfig&mode=1&usr=" + username + "&pwd=" + password);
|
||||
ipCameraHandler.setChannelState(CHANNEL_AUTO_LED, OnOffType.OFF);
|
||||
if (DecimalType.ZERO.equals(command) || OnOffType.OFF.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/CGIProxy.fcgi?cmd=closeInfraLed&usr=" + username + "&pwd=" + password);
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/CGIProxy.fcgi?cmd=openInfraLed&usr=" + username + "&pwd=" + password);
|
||||
}
|
||||
return;
|
||||
case CHANNEL_AUTO_LED:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_LED, UnDefType.UNDEF);
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/CGIProxy.fcgi?cmd=setInfraLedConfig&mode=0&usr=" + username + "&pwd=" + password);
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/CGIProxy.fcgi?cmd=setInfraLedConfig&mode=1&usr=" + username + "&pwd=" + password);
|
||||
}
|
||||
return;
|
||||
case CHANNEL_THRESHOLD_AUDIO_ALARM:
|
||||
int value = Math.round(Float.valueOf(command.toString()));
|
||||
if (value == 0) {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=setAudioAlarmConfig&isEnable=0&usr="
|
||||
+ username + "&pwd=" + password);
|
||||
} else if (value <= 33) {
|
||||
ipCameraHandler
|
||||
.sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=setAudioAlarmConfig&isEnable=1&sensitivity=0&usr="
|
||||
+ username + "&pwd=" + password);
|
||||
} else if (value <= 66) {
|
||||
ipCameraHandler
|
||||
.sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=setAudioAlarmConfig&isEnable=1&sensitivity=1&usr="
|
||||
+ username + "&pwd=" + password);
|
||||
} else {
|
||||
ipCameraHandler
|
||||
.sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=setAudioAlarmConfig&isEnable=1&sensitivity=2&usr="
|
||||
+ username + "&pwd=" + password);
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_AUDIO_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
if (ipCameraHandler.cameraConfig.getCustomAudioAlarmUrl().isEmpty()) {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=setAudioAlarmConfig&isEnable=1&usr="
|
||||
+ username + "&pwd=" + password);
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(ipCameraHandler.cameraConfig.getCustomAudioAlarmUrl());
|
||||
}
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=setAudioAlarmConfig&isEnable=0&usr="
|
||||
+ username + "&pwd=" + password);
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_MOTION_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
if (ipCameraHandler.cameraConfig.getCustomMotionAlarmUrl().isEmpty()) {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=setMotionDetectConfig&isEnable=1&usr="
|
||||
+ username + "&pwd=" + password);
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=setMotionDetectConfig1&isEnable=1&usr="
|
||||
+ username + "&pwd=" + password);
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(ipCameraHandler.cameraConfig.getCustomMotionAlarmUrl());
|
||||
}
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=setMotionDetectConfig&isEnable=0&usr="
|
||||
+ username + "&pwd=" + password);
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/CGIProxy.fcgi?cmd=setMotionDetectConfig1&isEnable=0&usr="
|
||||
+ username + "&pwd=" + password);
|
||||
}
|
||||
return;
|
||||
case CHANNEL_FFMPEG_MOTION_CONTROL:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.motionAlarmEnabled = true;
|
||||
} else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
|
||||
ipCameraHandler.motionAlarmEnabled = false;
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
|
||||
} else {
|
||||
ipCameraHandler.motionAlarmEnabled = true;
|
||||
ipCameraHandler.motionThreshold = Double.valueOf(command.toString());
|
||||
ipCameraHandler.motionThreshold = ipCameraHandler.motionThreshold / 10000;
|
||||
}
|
||||
ipCameraHandler.setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If a camera does not need to poll a request as often as snapshots, it can be
|
||||
// added here. Binding steps through the list.
|
||||
public ArrayList<String> getLowPriorityRequests() {
|
||||
ArrayList<String> lowPriorityRequests = new ArrayList<String>(1);
|
||||
lowPriorityRequests.add("/cgi-bin/CGIProxy.fcgi?cmd=getDevState&usr=" + username + "&pwd=" + password);
|
||||
return lowPriorityRequests;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.ipcamera.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link GroupConfig} handles the configuration of camera groups.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GroupConfig {
|
||||
private int pollTime, serverPort;
|
||||
private boolean motionChangesOrder = true;
|
||||
private String ipWhitelist = "";
|
||||
private String ffmpegLocation = "";
|
||||
private String ffmpegOutput = "";
|
||||
private String firstCamera = "";
|
||||
private String secondCamera = "";
|
||||
private String thirdCamera = "";
|
||||
private String forthCamera = "";
|
||||
|
||||
public String getFirstCamera() {
|
||||
return firstCamera;
|
||||
}
|
||||
|
||||
public String getSecondCamera() {
|
||||
return secondCamera;
|
||||
}
|
||||
|
||||
public String getThirdCamera() {
|
||||
return thirdCamera;
|
||||
}
|
||||
|
||||
public String getForthCamera() {
|
||||
return forthCamera;
|
||||
}
|
||||
|
||||
public boolean getMotionChangesOrder() {
|
||||
return motionChangesOrder;
|
||||
}
|
||||
|
||||
public String getIpWhitelist() {
|
||||
return ipWhitelist;
|
||||
}
|
||||
|
||||
public String getFfmpegLocation() {
|
||||
return ffmpegLocation;
|
||||
}
|
||||
|
||||
public String getFfmpegOutput() {
|
||||
return ffmpegOutput;
|
||||
}
|
||||
|
||||
public int getServerPort() {
|
||||
return serverPort;
|
||||
}
|
||||
|
||||
public int getPollTime() {
|
||||
return pollTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
|
||||
package org.openhab.binding.ipcamera.internal;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraGroupHandler;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
|
||||
/**
|
||||
* The {@link GroupTracker} is used so a 'group' thing can get a handle to each cameras handler, and the group and
|
||||
* cameras can talk to each other.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class GroupTracker {
|
||||
public ArrayList<IpCameraHandler> listOfOnlineCameraHandlers = new ArrayList<>(1);
|
||||
public ArrayList<IpCameraGroupHandler> listOfGroupHandlers = new ArrayList<>(0);
|
||||
public ArrayList<String> listOfOnlineCameraUID = new ArrayList<>(1);
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
|
||||
package org.openhab.binding.ipcamera.internal;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Enumeration;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link Helper} class has static functions that help the IpCamera binding not need as many external libs.
|
||||
*
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Helper {
|
||||
|
||||
/**
|
||||
* The {@link searchString} Used to grab values out of JSON or other quote encapsulated structures without needing
|
||||
* an external lib. String may be terminated by ," or }.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
public static String searchString(String rawString, String searchedString) {
|
||||
String result = "";
|
||||
int index = 0;
|
||||
index = rawString.indexOf(searchedString);
|
||||
if (index != -1) // -1 means "not found"
|
||||
{
|
||||
result = rawString.substring(index + searchedString.length(), rawString.length());
|
||||
index = result.indexOf(',');
|
||||
if (index == -1) {
|
||||
index = result.indexOf('"');
|
||||
if (index == -1) {
|
||||
index = result.indexOf('}');
|
||||
if (index == -1) {
|
||||
return result;
|
||||
} else {
|
||||
return result.substring(0, index);
|
||||
}
|
||||
} else {
|
||||
return result.substring(0, index);
|
||||
}
|
||||
} else {
|
||||
result = result.substring(0, index);
|
||||
index = result.indexOf('"');
|
||||
if (index == -1) {
|
||||
return result;
|
||||
} else {
|
||||
return result.substring(0, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static String fetchXML(String message, String sectionHeading, String key) {
|
||||
String result = "";
|
||||
int sectionHeaderBeginning = 0;
|
||||
if (!sectionHeading.isEmpty()) {// looking for a sectionHeading
|
||||
sectionHeaderBeginning = message.indexOf(sectionHeading);
|
||||
}
|
||||
if (sectionHeaderBeginning == -1) {
|
||||
return "";
|
||||
}
|
||||
int startIndex = message.indexOf(key, sectionHeaderBeginning + sectionHeading.length());
|
||||
if (startIndex == -1) {
|
||||
return "";
|
||||
}
|
||||
int endIndex = message.indexOf("<", startIndex + key.length());
|
||||
if (endIndex > startIndex) {
|
||||
result = message.substring(startIndex + key.length(), endIndex);
|
||||
}
|
||||
// remove any quotes and anything after the quote.
|
||||
sectionHeaderBeginning = result.indexOf("\"");
|
||||
if (sectionHeaderBeginning > 0) {
|
||||
result = result.substring(0, sectionHeaderBeginning);
|
||||
}
|
||||
// remove any ">" and anything after it.
|
||||
sectionHeaderBeginning = result.indexOf(">");
|
||||
if (sectionHeaderBeginning > 0) {
|
||||
result = result.substring(0, sectionHeaderBeginning);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link encodeSpecialChars} Is used to replace spaces with %20 in Strings meant for URL queries.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
public static String encodeSpecialChars(String text) {
|
||||
String processed = text;
|
||||
try {
|
||||
processed = URLEncoder.encode(text, "UTF-8").replace("+", "%20");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
|
||||
}
|
||||
return processed;
|
||||
}
|
||||
|
||||
public static String getLocalIpAddress() {
|
||||
String ipAddress = "";
|
||||
try {
|
||||
for (Enumeration<NetworkInterface> enumNetworks = NetworkInterface.getNetworkInterfaces(); enumNetworks
|
||||
.hasMoreElements();) {
|
||||
NetworkInterface networkInterface = enumNetworks.nextElement();
|
||||
for (Enumeration<InetAddress> enumIpAddr = networkInterface.getInetAddresses(); enumIpAddr
|
||||
.hasMoreElements();) {
|
||||
InetAddress inetAddress = enumIpAddr.nextElement();
|
||||
if (!inetAddress.isLoopbackAddress() && inetAddress.getHostAddress().toString().length() < 18
|
||||
&& inetAddress.isSiteLocalAddress()) {
|
||||
ipAddress = inetAddress.getHostAddress().toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SocketException ex) {
|
||||
}
|
||||
return ipAddress;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
|
||||
package org.openhab.binding.ipcamera.internal;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.http.DefaultFullHttpRequest;
|
||||
import io.netty.handler.codec.http.FullHttpRequest;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpHeaderValues;
|
||||
import io.netty.handler.codec.http.HttpMethod;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
/**
|
||||
* The {@link HikvisionHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class HikvisionHandler extends ChannelDuplexHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private IpCameraHandler ipCameraHandler;
|
||||
private int nvrChannel;
|
||||
private int lineCount, vmdCount, leftCount, takenCount, faceCount, pirCount, fieldCount;
|
||||
|
||||
public HikvisionHandler(ThingHandler handler, int nvrChannel) {
|
||||
ipCameraHandler = (IpCameraHandler) handler;
|
||||
this.nvrChannel = nvrChannel;
|
||||
}
|
||||
|
||||
// This handles the incoming http replies back from the camera.
|
||||
@Override
|
||||
public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
|
||||
if (msg == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
String content = "";
|
||||
int debounce = 3;
|
||||
try {
|
||||
content = msg.toString();
|
||||
if (content.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
logger.trace("HTTP Result back from camera is \t:{}:", content);
|
||||
|
||||
if (content.contains("--boundary")) {// Alarm checking goes in here//
|
||||
if (content.contains("<EventNotificationAlert version=\"")) {
|
||||
if (content.contains("hannelID>" + nvrChannel + "</")) {// some camera use c or <dynChannelID>
|
||||
if (content.contains("<eventType>linedetection</eventType>")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_LINE_CROSSING_ALARM);
|
||||
lineCount = debounce;
|
||||
}
|
||||
if (content.contains("<eventType>fielddetection</eventType>")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_FIELD_DETECTION_ALARM);
|
||||
fieldCount = debounce;
|
||||
}
|
||||
if (content.contains("<eventType>VMD</eventType>")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
|
||||
vmdCount = debounce;
|
||||
}
|
||||
if (content.contains("<eventType>facedetection</eventType>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_FACE_DETECTED, OnOffType.ON);
|
||||
faceCount = debounce;
|
||||
}
|
||||
if (content.contains("<eventType>unattendedBaggage</eventType>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ITEM_LEFT, OnOffType.ON);
|
||||
leftCount = debounce;
|
||||
}
|
||||
if (content.contains("<eventType>attendedBaggage</eventType>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ITEM_TAKEN, OnOffType.ON);
|
||||
takenCount = debounce;
|
||||
}
|
||||
if (content.contains("<eventType>PIR</eventType>")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_PIR_ALARM);
|
||||
pirCount = debounce;
|
||||
}
|
||||
if (content.contains("<eventType>videoloss</eventType>\r\n<eventState>inactive</eventState>")) {
|
||||
if (vmdCount > 1) {
|
||||
vmdCount = 1;
|
||||
}
|
||||
countDown();
|
||||
countDown();
|
||||
}
|
||||
} else if (content.contains("<channelID>0</channelID>")) {// NVR uses channel 0 to say all channels
|
||||
if (content.contains("<eventType>videoloss</eventType>\r\n<eventState>inactive</eventState>")) {
|
||||
if (vmdCount > 1) {
|
||||
vmdCount = 1;
|
||||
}
|
||||
countDown();
|
||||
countDown();
|
||||
}
|
||||
}
|
||||
countDown();
|
||||
}
|
||||
} else {
|
||||
String replyElement = Helper.fetchXML(content, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>", "<");
|
||||
switch (replyElement) {
|
||||
case "MotionDetection version=":
|
||||
ipCameraHandler.storeHttpReply(
|
||||
"/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection", content);
|
||||
if (content.contains("<enabled>true</enabled>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.ON);
|
||||
} else if (content.contains("<enabled>false</enabled>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "IOInputPort version=":
|
||||
ipCameraHandler.storeHttpReply("/ISAPI/System/IO/inputs/" + nvrChannel, content);
|
||||
if (content.contains("<enabled>true</enabled>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT, OnOffType.ON);
|
||||
} else if (content.contains("<enabled>false</enabled>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT, OnOffType.OFF);
|
||||
}
|
||||
if (content.contains("<triggering>low</triggering>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT, OnOffType.OFF);
|
||||
} else if (content.contains("<triggering>high</triggering>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT, OnOffType.ON);
|
||||
}
|
||||
break;
|
||||
case "LineDetection":
|
||||
ipCameraHandler.storeHttpReply("/ISAPI/Smart/LineDetection/" + nvrChannel + "01", content);
|
||||
if (content.contains("<enabled>true</enabled>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_LINE_CROSSING_ALARM, OnOffType.ON);
|
||||
} else if (content.contains("<enabled>false</enabled>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_LINE_CROSSING_ALARM, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "TextOverlay version=":
|
||||
ipCameraHandler.storeHttpReply(
|
||||
"/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1", content);
|
||||
String text = Helper.fetchXML(content, "<enabled>true</enabled>", "<displayText>");
|
||||
ipCameraHandler.setChannelState(CHANNEL_TEXT_OVERLAY, StringType.valueOf(text));
|
||||
break;
|
||||
case "AudioDetection version=":
|
||||
ipCameraHandler.storeHttpReply("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01",
|
||||
content);
|
||||
if (content.contains("<enabled>true</enabled>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.ON);
|
||||
} else if (content.contains("<enabled>false</enabled>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "IOPortStatus version=":
|
||||
if (content.contains("<ioState>active</ioState>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_EXTERNAL_ALARM_INPUT, OnOffType.ON);
|
||||
} else if (content.contains("<ioState>inactive</ioState>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_EXTERNAL_ALARM_INPUT, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "FieldDetection version=":
|
||||
ipCameraHandler.storeHttpReply("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01", content);
|
||||
if (content.contains("<enabled>true</enabled>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_FIELD_DETECTION_ALARM, OnOffType.ON);
|
||||
} else if (content.contains("<enabled>false</enabled>")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_FIELD_DETECTION_ALARM, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "ResponseStatus version=":
|
||||
////////////////// External Alarm Input ///////////////
|
||||
if (content.contains(
|
||||
"<requestURL>/ISAPI/System/IO/inputs/" + nvrChannel + "/status</requestURL>")) {
|
||||
// Stops checking the external alarm if camera does not have feature.
|
||||
if (content.contains("<statusString>Invalid Operation</statusString>")) {
|
||||
ipCameraHandler.lowPriorityRequests.remove(0);
|
||||
ipCameraHandler.logger.debug(
|
||||
"Stopping checks for alarm inputs as camera appears to be missing this feature.");
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (content.contains("<EventNotificationAlert")) {
|
||||
if (content.contains("hannelID>" + nvrChannel + "</")
|
||||
|| content.contains("<channelID>0</channelID>")) {// some camera use c or
|
||||
// <dynChannelID>
|
||||
if (content.contains(
|
||||
"<eventType>videoloss</eventType>\r\n<eventState>inactive</eventState>")) {
|
||||
if (vmdCount > 1) {
|
||||
vmdCount = 1;
|
||||
}
|
||||
countDown();
|
||||
countDown();
|
||||
}
|
||||
countDown();
|
||||
}
|
||||
} else {
|
||||
logger.debug("Unhandled reply-{}.", content);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// This does debouncing of the alarms
|
||||
void countDown() {
|
||||
|
||||
if (lineCount > 1) {
|
||||
lineCount--;
|
||||
} else if (lineCount == 1) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_LINE_CROSSING_ALARM, OnOffType.OFF);
|
||||
lineCount--;
|
||||
}
|
||||
if (vmdCount > 1) {
|
||||
vmdCount--;
|
||||
} else if (vmdCount == 1) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_MOTION_ALARM, OnOffType.OFF);
|
||||
vmdCount--;
|
||||
}
|
||||
if (leftCount > 1) {
|
||||
leftCount--;
|
||||
} else if (leftCount == 1) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ITEM_LEFT, OnOffType.OFF);
|
||||
leftCount--;
|
||||
}
|
||||
if (takenCount > 1) {
|
||||
takenCount--;
|
||||
} else if (takenCount == 1) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ITEM_TAKEN, OnOffType.OFF);
|
||||
takenCount--;
|
||||
}
|
||||
if (faceCount > 1) {
|
||||
faceCount--;
|
||||
} else if (faceCount == 1) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_FACE_DETECTED, OnOffType.OFF);
|
||||
faceCount--;
|
||||
}
|
||||
if (pirCount > 1) {
|
||||
pirCount--;
|
||||
} else if (pirCount == 1) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_PIR_ALARM, OnOffType.OFF);
|
||||
pirCount--;
|
||||
}
|
||||
if (fieldCount > 1) {
|
||||
fieldCount--;
|
||||
} else if (fieldCount == 1) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_FIELD_DETECTION_ALARM, OnOffType.OFF);
|
||||
fieldCount--;
|
||||
}
|
||||
if (fieldCount == 0 && pirCount == 0 && faceCount == 0 && takenCount == 0 && leftCount == 0 && vmdCount == 0
|
||||
&& lineCount == 0) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
|
||||
}
|
||||
}
|
||||
|
||||
public void hikSendXml(String httpPutURL, String xml) {
|
||||
logger.trace("Body for PUT:{} is going to be:{}", httpPutURL, xml);
|
||||
FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"), httpPutURL);
|
||||
request.headers().set(HttpHeaderNames.HOST, ipCameraHandler.cameraConfig.getIp());
|
||||
request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
|
||||
request.headers().add(HttpHeaderNames.CONTENT_TYPE, "application/xml; charset=\"UTF-8\"");
|
||||
ByteBuf bbuf = Unpooled.copiedBuffer(xml, StandardCharsets.UTF_8);
|
||||
request.headers().set(HttpHeaderNames.CONTENT_LENGTH, bbuf.readableBytes());
|
||||
request.content().clear().writeBytes(bbuf);
|
||||
ipCameraHandler.sendHttpPUT(httpPutURL, request);
|
||||
}
|
||||
|
||||
public void hikChangeSetting(String httpGetPutURL, String removeElement, String replaceRemovedElementWith) {
|
||||
ChannelTracking localTracker = ipCameraHandler.channelTrackingMap.get(httpGetPutURL);
|
||||
if (localTracker == null) {
|
||||
ipCameraHandler.sendHttpGET(httpGetPutURL);
|
||||
logger.debug(
|
||||
"Did not have a reply stored before hikChangeSetting was run, try again shortly as a reply has just been requested.");
|
||||
return;
|
||||
}
|
||||
String body = localTracker.getReply();
|
||||
if (body.isEmpty()) {
|
||||
logger.debug(
|
||||
"Did not have a reply stored before hikChangeSetting was run, try again shortly as a reply has just been requested.");
|
||||
ipCameraHandler.sendHttpGET(httpGetPutURL);
|
||||
} else {
|
||||
logger.trace("An OLD reply from the camera was:{}", body);
|
||||
if (body.contains("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")) {
|
||||
body = body.substring("<?xml version=\"1.0\" encoding=\"UTF-8\"?>".length());
|
||||
}
|
||||
int elementIndexStart = body.indexOf("<" + removeElement + ">");
|
||||
int elementIndexEnd = body.indexOf("</" + removeElement + ">");
|
||||
body = body.substring(0, elementIndexStart) + replaceRemovedElementWith
|
||||
+ body.substring(elementIndexEnd + removeElement.length() + 3, body.length());
|
||||
logger.trace("Body for this PUT is going to be:{}", body);
|
||||
localTracker.setReply(body);
|
||||
FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("PUT"),
|
||||
httpGetPutURL);
|
||||
request.headers().set(HttpHeaderNames.HOST, ipCameraHandler.cameraConfig.getIp());
|
||||
request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
|
||||
request.headers().add(HttpHeaderNames.CONTENT_TYPE, "application/xml; charset=\"UTF-8\"");
|
||||
ByteBuf bbuf = Unpooled.copiedBuffer(body, StandardCharsets.UTF_8);
|
||||
request.headers().set(HttpHeaderNames.CONTENT_LENGTH, bbuf.readableBytes());
|
||||
request.content().clear().writeBytes(bbuf);
|
||||
ipCameraHandler.sendHttpPUT(httpGetPutURL, request);
|
||||
}
|
||||
}
|
||||
|
||||
// This handles the commands that come from the Openhab event bus.
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_ENABLE_AUDIO_ALARM:
|
||||
ipCameraHandler.sendHttpGET("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01");
|
||||
return;
|
||||
case CHANNEL_ENABLE_LINE_CROSSING_ALARM:
|
||||
ipCameraHandler.sendHttpGET("/ISAPI/Smart/LineDetection/" + nvrChannel + "01");
|
||||
return;
|
||||
case CHANNEL_ENABLE_FIELD_DETECTION_ALARM:
|
||||
ipCameraHandler.logger.debug("FieldDetection command");
|
||||
ipCameraHandler.sendHttpGET("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01");
|
||||
return;
|
||||
case CHANNEL_ENABLE_MOTION_ALARM:
|
||||
ipCameraHandler
|
||||
.sendHttpGET("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection");
|
||||
return;
|
||||
case CHANNEL_TEXT_OVERLAY:
|
||||
ipCameraHandler
|
||||
.sendHttpGET("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1");
|
||||
return;
|
||||
case CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT:
|
||||
ipCameraHandler.sendHttpGET("/ISAPI/System/IO/inputs/" + nvrChannel);
|
||||
return;
|
||||
case CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT:
|
||||
ipCameraHandler.sendHttpGET("/ISAPI/System/IO/inputs/" + nvrChannel);
|
||||
return;
|
||||
}
|
||||
return; // Return as we have handled the refresh command above and don't need to
|
||||
// continue further.
|
||||
} // end of "REFRESH"
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_TEXT_OVERLAY:
|
||||
logger.debug("Changing text overlay to {}", command.toString());
|
||||
if (command.toString().isEmpty()) {
|
||||
hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1",
|
||||
"enabled", "<enabled>false</enabled>");
|
||||
} else {
|
||||
hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1",
|
||||
"displayText", "<displayText>" + command.toString() + "</displayText>");
|
||||
hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "/overlays/text/1",
|
||||
"enabled", "<enabled>true</enabled>");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT:
|
||||
logger.debug("Changing enabled state of the external input 1 to {}", command.toString());
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "enabled", "<enabled>true</enabled>");
|
||||
} else {
|
||||
hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "enabled", "<enabled>false</enabled>");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT:
|
||||
logger.debug("Changing triggering state of the external input 1 to {}", command.toString());
|
||||
if (OnOffType.OFF.equals(command)) {
|
||||
hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "triggering",
|
||||
"<triggering>low</triggering>");
|
||||
} else {
|
||||
hikChangeSetting("/ISAPI/System/IO/inputs/" + nvrChannel, "triggering",
|
||||
"<triggering>high</triggering>");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_PIR_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
hikChangeSetting("/ISAPI/WLAlarm/PIR", "enabled", "<enabled>true</enabled>");
|
||||
} else {
|
||||
hikChangeSetting("/ISAPI/WLAlarm/PIR", "enabled", "<enabled>false</enabled>");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_AUDIO_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
hikChangeSetting("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01", "enabled",
|
||||
"<enabled>true</enabled>");
|
||||
} else {
|
||||
hikChangeSetting("/ISAPI/Smart/AudioDetection/channels/" + nvrChannel + "01", "enabled",
|
||||
"<enabled>false</enabled>");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_LINE_CROSSING_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
hikChangeSetting("/ISAPI/Smart/LineDetection/" + nvrChannel + "01", "enabled",
|
||||
"<enabled>true</enabled>");
|
||||
} else {
|
||||
hikChangeSetting("/ISAPI/Smart/LineDetection/" + nvrChannel + "01", "enabled",
|
||||
"<enabled>false</enabled>");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_MOTION_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection",
|
||||
"enabled", "<enabled>true</enabled>");
|
||||
} else {
|
||||
hikChangeSetting("/ISAPI/System/Video/inputs/channels/" + nvrChannel + "01/motionDetection",
|
||||
"enabled", "<enabled>false</enabled>");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_FIELD_DETECTION_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
hikChangeSetting("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01", "enabled",
|
||||
"<enabled>true</enabled>");
|
||||
} else {
|
||||
hikChangeSetting("/ISAPI/Smart/FieldDetection/" + nvrChannel + "01", "enabled",
|
||||
"<enabled>false</enabled>");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ACTIVATE_ALARM_OUTPUT:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
hikSendXml("/ISAPI/System/IO/outputs/" + nvrChannel + "/trigger",
|
||||
"<IOPortData version=\"1.0\" xmlns=\"http://www.hikvision.com/ver10/XMLSchema\">\r\n <outputState>high</outputState>\r\n</IOPortData>\r\n");
|
||||
} else {
|
||||
hikSendXml("/ISAPI/System/IO/outputs/" + nvrChannel + "/trigger",
|
||||
"<IOPortData version=\"1.0\" xmlns=\"http://www.hikvision.com/ver10/XMLSchema\">\r\n <outputState>low</outputState>\r\n</IOPortData>\r\n");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_FFMPEG_MOTION_CONTROL:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.motionAlarmEnabled = true;
|
||||
} else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
|
||||
ipCameraHandler.motionAlarmEnabled = false;
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
|
||||
} else {
|
||||
ipCameraHandler.motionAlarmEnabled = true;
|
||||
ipCameraHandler.motionThreshold = Double.valueOf(command.toString());
|
||||
ipCameraHandler.motionThreshold = ipCameraHandler.motionThreshold / 10000;
|
||||
}
|
||||
ipCameraHandler.setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If a camera does not need to poll a request as often as snapshots, it can be
|
||||
// added here. Binding steps through the list.
|
||||
public ArrayList<String> getLowPriorityRequests() {
|
||||
ArrayList<String> lowPriorityRequests = new ArrayList<String>(1);
|
||||
lowPriorityRequests.add("/ISAPI/System/IO/inputs/" + nvrChannel + "/status"); // must stay in element 0.
|
||||
return lowPriorityRequests;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
|
||||
package org.openhab.binding.ipcamera.internal;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.CHANNEL_THRESHOLD_AUDIO_ALARM;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
/**
|
||||
* The {@link HttpOnlyHandler} is responsible for handling commands for generic and onvif thing types.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class HttpOnlyHandler extends ChannelDuplexHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private IpCameraHandler ipCameraHandler;
|
||||
|
||||
public HttpOnlyHandler(IpCameraHandler handler) {
|
||||
ipCameraHandler = handler;
|
||||
}
|
||||
|
||||
// This handles the incoming http replies back from the camera.
|
||||
@Override
|
||||
public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
|
||||
// This handles the commands that come from the Openhab event bus.
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
return; // Return as we have handled the refresh command above and don't need to
|
||||
// continue further.
|
||||
} // end of "REFRESH"
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_THRESHOLD_AUDIO_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.audioAlarmEnabled = true;
|
||||
} else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
|
||||
ipCameraHandler.audioAlarmEnabled = false;
|
||||
} else {
|
||||
ipCameraHandler.audioAlarmEnabled = true;
|
||||
try {
|
||||
ipCameraHandler.audioThreshold = Integer.valueOf(command.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
logger.warn("Audio Threshold recieved an unexpected command, was it a number?");
|
||||
}
|
||||
}
|
||||
ipCameraHandler.setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If a camera does not need to poll a request as often as snapshots, it can be
|
||||
// added here. Binding steps through the list and sends 1 every 8 seconds.
|
||||
public ArrayList<String> getLowPriorityRequests() {
|
||||
return new ArrayList<String>(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
|
||||
package org.openhab.binding.ipcamera.internal;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
/**
|
||||
* The {@link InstarHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class InstarHandler extends ChannelDuplexHandler {
|
||||
private IpCameraHandler ipCameraHandler;
|
||||
private String requestUrl = "Empty";
|
||||
|
||||
public InstarHandler(ThingHandler thingHandler) {
|
||||
ipCameraHandler = (IpCameraHandler) thingHandler;
|
||||
}
|
||||
|
||||
public void setURL(String url) {
|
||||
requestUrl = url;
|
||||
}
|
||||
|
||||
// This handles the incoming http replies back from the camera.
|
||||
@Override
|
||||
public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
|
||||
if (msg == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
String content = "";
|
||||
String value1 = "";
|
||||
try {
|
||||
content = msg.toString();
|
||||
if (content.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
switch (requestUrl) {
|
||||
case "/param.cgi?cmd=getinfrared":
|
||||
if (content.contains("var infraredstat=\"auto")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_AUTO_LED, OnOffType.ON);
|
||||
} else {
|
||||
ipCameraHandler.setChannelState(CHANNEL_AUTO_LED, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "/param.cgi?cmd=getoverlayattr&-region=1":// Text Overlays
|
||||
if (content.contains("var show_1=\"0\"")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_TEXT_OVERLAY, StringType.EMPTY);
|
||||
} else {
|
||||
value1 = Helper.searchString(content, "var name_1=\"");
|
||||
if (!value1.isEmpty()) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_TEXT_OVERLAY, StringType.valueOf(value1));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "/cgi-bin/hi3510/param.cgi?cmd=getmdattr":// Motion Alarm
|
||||
// Motion Alarm
|
||||
if (content.contains("var m1_enable=\"1\"")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.ON);
|
||||
} else {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_MOTION_ALARM, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "/cgi-bin/hi3510/param.cgi?cmd=getaudioalarmattr":// Audio Alarm
|
||||
if (content.contains("var aa_enable=\"1\"")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.ON);
|
||||
value1 = Helper.searchString(content, "var aa_value=\"");
|
||||
if (!value1.isEmpty()) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_THRESHOLD_AUDIO_ALARM, PercentType.valueOf(value1));
|
||||
}
|
||||
} else {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_AUDIO_ALARM, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "param.cgi?cmd=getpirattr":// PIR Alarm
|
||||
if (content.contains("var pir_enable=\"1\"")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_PIR_ALARM, OnOffType.ON);
|
||||
} else {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_PIR_ALARM, OnOffType.OFF);
|
||||
}
|
||||
// Reset the Alarm, need to find better place to put this.
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_PIR_ALARM);
|
||||
break;
|
||||
case "/param.cgi?cmd=getioattr":// External Alarm Input
|
||||
if (content.contains("var io_enable=\"1\"")) {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT, OnOffType.ON);
|
||||
} else {
|
||||
ipCameraHandler.setChannelState(CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// This handles the commands that come from the Openhab event bus.
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_MOTION_ALARM:
|
||||
if (ipCameraHandler.cameraConfig.getServerPort() > 0) {
|
||||
ipCameraHandler.logger.info("Setting up the Alarm Server settings in the camera now");
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/param.cgi?cmd=setmdalarm&-aname=server2&-switch=on&-interval=1&cmd=setalarmserverattr&-as_index=3&-as_server="
|
||||
+ ipCameraHandler.hostIp + "&-as_port="
|
||||
+ ipCameraHandler.cameraConfig.getServerPort()
|
||||
+ "&-as_path=/instar&-as_queryattr1=&-as_queryval1=&-as_queryattr2=&-as_queryval2=&-as_queryattr3=&-as_queryval3=&-as_activequery=1&-as_auth=0&-as_query1=0&-as_query2=0&-as_query3=0");
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
} // end of "REFRESH"
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_THRESHOLD_AUDIO_ALARM:
|
||||
int value = Math.round(Float.valueOf(command.toString()));
|
||||
if (value == 0) {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/hi3510/param.cgi?cmd=setaudioalarmattr&-aa_enable=0");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/hi3510/param.cgi?cmd=setaudioalarmattr&-aa_enable=1");
|
||||
ipCameraHandler
|
||||
.sendHttpGET("/cgi-bin/hi3510/param.cgi?cmd=setaudioalarmattr&-aa_enable=1&-aa_value="
|
||||
+ command.toString());
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_AUDIO_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/hi3510/param.cgi?cmd=setaudioalarmattr&-aa_enable=1");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET("/cgi-bin/hi3510/param.cgi?cmd=setaudioalarmattr&-aa_enable=0");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_MOTION_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/hi3510/param.cgi?cmd=setmdattr&-enable=1&-name=1&cmd=setmdattr&-enable=1&-name=2&cmd=setmdattr&-enable=1&-name=3&cmd=setmdattr&-enable=1&-name=4");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET(
|
||||
"/cgi-bin/hi3510/param.cgi?cmd=setmdattr&-enable=0&-name=1&cmd=setmdattr&-enable=0&-name=2&cmd=setmdattr&-enable=0&-name=3&cmd=setmdattr&-enable=0&-name=4");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_TEXT_OVERLAY:
|
||||
String text = Helper.encodeSpecialChars(command.toString());
|
||||
if (text.isEmpty()) {
|
||||
ipCameraHandler.sendHttpGET("/param.cgi?cmd=setoverlayattr&-region=1&-show=0");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET("/param.cgi?cmd=setoverlayattr&-region=1&-show=1&-name=" + text);
|
||||
}
|
||||
return;
|
||||
case CHANNEL_AUTO_LED:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET("/param.cgi?cmd=setinfrared&-infraredstat=auto");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET("/param.cgi?cmd=setinfrared&-infraredstat=close");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_PIR_ALARM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET("/param.cgi?cmd=setpirattr&-pir_enable=1");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET("/param.cgi?cmd=setpirattr&-pir_enable=0");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.sendHttpGET("/param.cgi?cmd=setioattr&-io_enable=1");
|
||||
} else {
|
||||
ipCameraHandler.sendHttpGET("/param.cgi?cmd=setioattr&-io_enable=0");
|
||||
}
|
||||
return;
|
||||
case CHANNEL_FFMPEG_MOTION_CONTROL:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
ipCameraHandler.motionAlarmEnabled = true;
|
||||
} else if (OnOffType.OFF.equals(command) || DecimalType.ZERO.equals(command)) {
|
||||
ipCameraHandler.motionAlarmEnabled = false;
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
|
||||
} else {
|
||||
ipCameraHandler.motionAlarmEnabled = true;
|
||||
ipCameraHandler.motionThreshold = Double.valueOf(command.toString());
|
||||
ipCameraHandler.motionThreshold = ipCameraHandler.motionThreshold / 10000;
|
||||
}
|
||||
ipCameraHandler.setupFfmpegFormat(FFmpegFormat.RTSP_ALARMS);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void alarmTriggered(String alarm) {
|
||||
ipCameraHandler.logger.debug("Alarm has been triggered:{}", alarm);
|
||||
switch (alarm) {
|
||||
case "/instar?&active=1":// The motion area boxes 1-4
|
||||
case "/instar?&active=2":
|
||||
case "/instar?&active=3":
|
||||
case "/instar?&active=4":
|
||||
ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
|
||||
break;
|
||||
case "/instar?&active=5":// PIR
|
||||
ipCameraHandler.motionDetected(CHANNEL_PIR_ALARM);
|
||||
break;
|
||||
case "/instar?&active=6":// Audio Alarm
|
||||
ipCameraHandler.audioDetected();
|
||||
break;
|
||||
case "/instar?&active=7":// Motion Area 1
|
||||
case "/instar?&active=8":// Motion Area 2
|
||||
case "/instar?&active=9":// Motion Area 3
|
||||
case "/instar?&active=10":// Motion Area 4
|
||||
ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If a camera does not need to poll a request as often as snapshots, it can be
|
||||
// added here. Binding steps through the list.
|
||||
public ArrayList<String> getLowPriorityRequests() {
|
||||
ArrayList<String> lowPriorityRequests = new ArrayList<String>(2);
|
||||
lowPriorityRequests.add("/cgi-bin/hi3510/param.cgi?cmd=getaudioalarmattr");
|
||||
lowPriorityRequests.add("/cgi-bin/hi3510/param.cgi?cmd=getmdattr");
|
||||
lowPriorityRequests.add("/param.cgi?cmd=getinfrared");
|
||||
lowPriorityRequests.add("/param.cgi?cmd=getoverlayattr&-region=1");
|
||||
lowPriorityRequests.add("/param.cgi?cmd=getpirattr");
|
||||
lowPriorityRequests.add("/param.cgi?cmd=getioattr"); // ext alarm input on/off
|
||||
// lowPriorityRequests.add("/param.cgi?cmd=getserverinfo");
|
||||
return lowPriorityRequests;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.ipcamera.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.openhab.core.automation.annotation.ActionInput;
|
||||
import org.openhab.core.automation.annotation.RuleAction;
|
||||
import org.openhab.core.thing.binding.ThingActions;
|
||||
import org.openhab.core.thing.binding.ThingActionsScope;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link IpCameraActions} is responsible for Actions.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@ThingActionsScope(name = "ipcamera")
|
||||
@NonNullByDefault
|
||||
public class IpCameraActions implements ThingActions {
|
||||
public final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private @Nullable IpCameraHandler handler;
|
||||
|
||||
@Override
|
||||
public void setThingHandler(@Nullable ThingHandler handler) {
|
||||
this.handler = (IpCameraHandler) handler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingHandler getThingHandler() {
|
||||
return handler;
|
||||
}
|
||||
|
||||
@RuleAction(label = "Record MP4", description = "Record MP4 to a set filename if given, or if filename is null to ipcamera.mp4")
|
||||
public void recordMP4(
|
||||
@ActionInput(name = "filename", label = "Filename", description = "Name that the recording will have once created, don't include the .mp4.") @Nullable String filename,
|
||||
@ActionInput(name = "secondsToRecord", label = "Seconds to Record", description = "Enter a number of how many seconds to record.") int secondsToRecord) {
|
||||
logger.debug("Recording {}.mp4 for {} seconds.", filename, secondsToRecord);
|
||||
if (filename == null && handler != null) {
|
||||
handler.recordMp4("ipcamera", secondsToRecord);
|
||||
} else if (handler != null && filename != null) {
|
||||
handler.recordMp4(filename, secondsToRecord);
|
||||
}
|
||||
}
|
||||
|
||||
public static void recordMP4(@Nullable ThingActions actions, @Nullable String filename, int secondsToRecord) {
|
||||
if (actions instanceof IpCameraActions) {
|
||||
((IpCameraActions) actions).recordMP4(filename, secondsToRecord);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Instance is not a IpCamera class.");
|
||||
}
|
||||
}
|
||||
|
||||
@RuleAction(label = "Record GIF", description = "Record GIF to a set filename if given, or if filename is null to ipcamera.gif")
|
||||
public void recordGIF(
|
||||
@ActionInput(name = "filename", label = "Filename", description = "Name that the recording will have once created, don't include the .mp4.") @Nullable String filename,
|
||||
@ActionInput(name = "secondsToRecord", label = "Seconds to Record", description = "Enter a number of how many seconds to record.") int secondsToRecord) {
|
||||
logger.debug("Recording {}.gif for {} seconds.", filename, secondsToRecord);
|
||||
if (filename == null && handler != null) {
|
||||
handler.recordGif("ipcamera", secondsToRecord);
|
||||
} else if (handler != null && filename != null) {
|
||||
handler.recordGif(filename, secondsToRecord);
|
||||
}
|
||||
}
|
||||
|
||||
public static void recordGIF(@Nullable ThingActions actions, @Nullable String filename, int secondsToRecord) {
|
||||
if (actions instanceof IpCameraActions) {
|
||||
((IpCameraActions) actions).recordGIF(filename, secondsToRecord);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Instance is not a IpCamera class.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
|
||||
package org.openhab.binding.ipcamera.internal;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link IpCameraBindingConstants} class defines common constants, which
|
||||
* are used across the whole binding.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class IpCameraBindingConstants {
|
||||
|
||||
private static final String BINDING_ID = "ipcamera";
|
||||
public final static String AUTH_HANDLER = "authorizationHandler";
|
||||
public final static String AMCREST_HANDLER = "amcrestHandler";
|
||||
public final static String COMMON_HANDLER = "commonHandler";
|
||||
public final static String INSTAR_HANDLER = "instarHandler";
|
||||
|
||||
public static enum FFmpegFormat {
|
||||
HLS,
|
||||
GIF,
|
||||
RECORD,
|
||||
RTSP_ALARMS,
|
||||
MJPEG,
|
||||
SNAPSHOT
|
||||
}
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_GROUP = new ThingTypeUID(BINDING_ID, "group");
|
||||
public static final String GENERIC_THING = "generic";
|
||||
public static final ThingTypeUID THING_TYPE_GENERIC = new ThingTypeUID(BINDING_ID, GENERIC_THING);
|
||||
public static final String ONVIF_THING = "onvif";
|
||||
public static final ThingTypeUID THING_TYPE_ONVIF = new ThingTypeUID(BINDING_ID, ONVIF_THING);
|
||||
public static final String AMCREST_THING = "amcrest";
|
||||
public static final ThingTypeUID THING_TYPE_AMCREST = new ThingTypeUID(BINDING_ID, AMCREST_THING);
|
||||
public static final String FOSCAM_THING = "foscam";
|
||||
public static final ThingTypeUID THING_TYPE_FOSCAM = new ThingTypeUID(BINDING_ID, FOSCAM_THING);
|
||||
public static final String HIKVISION_THING = "hikvision";
|
||||
public static final ThingTypeUID THING_TYPE_HIKVISION = new ThingTypeUID(BINDING_ID, HIKVISION_THING);
|
||||
public static final String INSTAR_THING = "instar";
|
||||
public static final ThingTypeUID THING_TYPE_INSTAR = new ThingTypeUID(BINDING_ID, INSTAR_THING);
|
||||
public static final String DAHUA_THING = "dahua";
|
||||
public static final ThingTypeUID THING_TYPE_DAHUA = new ThingTypeUID(BINDING_ID, DAHUA_THING);
|
||||
public static final String DOORBIRD_THING = "doorbird";
|
||||
public static final ThingTypeUID THING_TYPE_DOORBIRD = new ThingTypeUID(BINDING_ID, DOORBIRD_THING);
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = new HashSet<ThingTypeUID>(
|
||||
Arrays.asList(THING_TYPE_ONVIF, THING_TYPE_GENERIC, THING_TYPE_AMCREST, THING_TYPE_DAHUA, THING_TYPE_INSTAR,
|
||||
THING_TYPE_FOSCAM, THING_TYPE_DOORBIRD, THING_TYPE_HIKVISION));
|
||||
|
||||
public static final Set<ThingTypeUID> GROUP_SUPPORTED_THING_TYPES = new HashSet<ThingTypeUID>(
|
||||
Arrays.asList(THING_TYPE_GROUP));
|
||||
|
||||
// List of all Thing Config items
|
||||
public static final String CONFIG_IPADDRESS = "ipAddress";
|
||||
public static final String CONFIG_PORT = "port";
|
||||
public static final String CONFIG_ONVIF_PORT = "onvifPort";
|
||||
public static final String CONFIG_SERVER_PORT = "serverPort";
|
||||
public static final String CONFIG_USERNAME = "username";
|
||||
public static final String CONFIG_PASSWORD = "password";
|
||||
public static final String CONFIG_ONVIF_PROFILE_NUMBER = "onvifMediaProfile";
|
||||
public static final String CONFIG_POLL_TIME = "pollTime";
|
||||
public static final String CONFIG_FFMPEG_INPUT = "ffmpegInput";
|
||||
public static final String CONFIG_SNAPSHOT_URL_OVERRIDE = "snapshotUrl";
|
||||
public static final String CONFIG_MJPEG_URL = "mjpegUrl";
|
||||
public static final String CONFIG_FFMPEG_MOTION_INPUT = "alarmInputUrl";
|
||||
public static final String CONFIG_MOTION_URL_OVERRIDE = "customMotionAlarmUrl";
|
||||
public static final String CONFIG_AUDIO_URL_OVERRIDE = "customAudioAlarmUrl";
|
||||
public static final String CONFIG_IMAGE_UPDATE_WHEN = "updateImageWhen";
|
||||
public static final String CONFIG_NVR_CHANNEL = "nvrChannel";
|
||||
public static final String CONFIG_IP_WHITELIST = "ipWhitelist";
|
||||
public static final String CONFIG_FFMPEG_LOCATION = "ffmpegLocation";
|
||||
public static final String CONFIG_FFMPEG_OUTPUT = "ffmpegOutput";
|
||||
public static final String CONFIG_FFMPEG_HLS_OUT_ARGUMENTS = "hlsOutOptions";
|
||||
public static final String CONFIG_FFMPEG_GIF_OUT_ARGUMENTS = "gifOutOptions";
|
||||
public static final String CONFIG_FFMPEG_MP4_OUT_ARGUMENTS = "mp4OutOptions";
|
||||
public static final String CONFIG_FFMPEG_MJPEG_ARGUMENTS = "mjpegOptions";
|
||||
public static final String CONFIG_FFMPEG_MOTION_ARGUMENTS = "motionOptions";
|
||||
public static final String CONFIG_PTZ_CONTINUOUS = "ptzContinuous";
|
||||
public static final String CONFIG_GIF_PREROLL = "gifPreroll";
|
||||
// group thing configs
|
||||
public static final String CONFIG_FIRST_CAM = "firstCamera";
|
||||
public static final String CONFIG_SECOND_CAM = "secondCamera";
|
||||
public static final String CONFIG_THIRD_CAM = "thirdCamera";
|
||||
public static final String CONFIG_FORTH_CAM = "forthCamera";
|
||||
public static final String CONFIG_MOTION_CHANGES_ORDER = "motionChangesOrder";
|
||||
|
||||
// List of all Channel ids
|
||||
public static final String CHANNEL_POLL_IMAGE = "pollImage";
|
||||
public static final String CHANNEL_RECORDING_GIF = "recordingGif";
|
||||
public static final String CHANNEL_GIF_HISTORY = "gifHistory";
|
||||
public static final String CHANNEL_GIF_HISTORY_LENGTH = "gifHistoryLength";
|
||||
public static final String CHANNEL_RECORDING_MP4 = "recordingMp4";
|
||||
public static final String CHANNEL_MP4_PREROLL = "mp4Preroll";
|
||||
public static final String CHANNEL_MP4_HISTORY = "mp4History";
|
||||
public static final String CHANNEL_MP4_HISTORY_LENGTH = "mp4HistoryLength";
|
||||
public static final String CHANNEL_IMAGE = "image";
|
||||
public static final String CHANNEL_RTSP_URL = "rtspUrl";
|
||||
public static final String CHANNEL_IMAGE_URL = "imageUrl";
|
||||
public static final String CHANNEL_MJPEG_URL = "mjpegUrl";
|
||||
public static final String CHANNEL_HLS_URL = "hlsUrl";
|
||||
public static final String CHANNEL_PAN = "pan";
|
||||
public static final String CHANNEL_TILT = "tilt";
|
||||
public static final String CHANNEL_ZOOM = "zoom";
|
||||
public static final String CHANNEL_EXTERNAL_MOTION = "externalMotion";
|
||||
public static final String CHANNEL_MOTION_ALARM = "motionAlarm";
|
||||
public static final String CHANNEL_LINE_CROSSING_ALARM = "lineCrossingAlarm";
|
||||
public static final String CHANNEL_FACE_DETECTED = "faceDetected";
|
||||
public static final String CHANNEL_ITEM_LEFT = "itemLeft";
|
||||
public static final String CHANNEL_ITEM_TAKEN = "itemTaken";
|
||||
public static final String CHANNEL_AUDIO_ALARM = "audioAlarm";
|
||||
public static final String CHANNEL_ENABLE_MOTION_ALARM = "enableMotionAlarm";
|
||||
public static final String CHANNEL_FFMPEG_MOTION_CONTROL = "ffmpegMotionControl";
|
||||
public static final String CHANNEL_FFMPEG_MOTION_ALARM = "ffmpegMotionAlarm";
|
||||
public static final String CHANNEL_ENABLE_LINE_CROSSING_ALARM = "enableLineCrossingAlarm";
|
||||
public static final String CHANNEL_ENABLE_AUDIO_ALARM = "enableAudioAlarm";
|
||||
public static final String CHANNEL_THRESHOLD_AUDIO_ALARM = "thresholdAudioAlarm";
|
||||
public static final String CHANNEL_ACTIVATE_ALARM_OUTPUT = "activateAlarmOutput";
|
||||
public static final String CHANNEL_ACTIVATE_ALARM_OUTPUT2 = "activateAlarmOutput2";
|
||||
public static final String CHANNEL_ENABLE_EXTERNAL_ALARM_INPUT = "enableExternalAlarmInput";
|
||||
public static final String CHANNEL_TRIGGER_EXTERNAL_ALARM_INPUT = "triggerExternalAlarmInput";
|
||||
public static final String CHANNEL_EXTERNAL_ALARM_INPUT = "externalAlarmInput";
|
||||
public static final String CHANNEL_EXTERNAL_ALARM_INPUT2 = "externalAlarmInput2";
|
||||
public static final String CHANNEL_AUTO_LED = "autoLED";
|
||||
public static final String CHANNEL_ENABLE_LED = "enableLED";
|
||||
public static final String CHANNEL_ENABLE_PIR_ALARM = "enablePirAlarm";
|
||||
public static final String CHANNEL_PIR_ALARM = "pirAlarm";
|
||||
public static final String CHANNEL_CELL_MOTION_ALARM = "cellMotionAlarm";
|
||||
public static final String CHANNEL_ENABLE_FIELD_DETECTION_ALARM = "enableFieldDetectionAlarm";
|
||||
public static final String CHANNEL_FIELD_DETECTION_ALARM = "fieldDetectionAlarm";
|
||||
public static final String CHANNEL_PARKING_ALARM = "parkingAlarm";
|
||||
public static final String CHANNEL_TAMPER_ALARM = "tamperAlarm";
|
||||
public static final String CHANNEL_TOO_DARK_ALARM = "tooDarkAlarm";
|
||||
public static final String CHANNEL_STORAGE_ALARM = "storageAlarm";
|
||||
public static final String CHANNEL_SCENE_CHANGE_ALARM = "sceneChangeAlarm";
|
||||
public static final String CHANNEL_TOO_BRIGHT_ALARM = "tooBrightAlarm";
|
||||
public static final String CHANNEL_TOO_BLURRY_ALARM = "tooBlurryAlarm";
|
||||
public static final String CHANNEL_TEXT_OVERLAY = "textOverlay";
|
||||
public static final String CHANNEL_EXTERNAL_LIGHT = "externalLight";
|
||||
public static final String CHANNEL_DOORBELL = "doorBell";
|
||||
public static final String CHANNEL_LAST_MOTION_TYPE = "lastMotionType";
|
||||
public static final String CHANNEL_GOTO_PRESET = "gotoPreset";
|
||||
public static final String CHANNEL_START_STREAM = "startStream";
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
|
||||
package org.openhab.binding.ipcamera.internal;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
|
||||
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.ipcamera.internal.onvif.OnvifDiscovery;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link IpCameraDiscoveryService} is responsible for auto finding cameras that have Onvif
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "binding.ipcamera")
|
||||
public class IpCameraDiscoveryService extends AbstractDiscoveryService {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(IpCameraDiscoveryService.class);
|
||||
|
||||
public IpCameraDiscoveryService() {
|
||||
super(SUPPORTED_THING_TYPES, 30, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startBackgroundDiscovery() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deactivate() {
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
public void newCameraFound(String brand, String hostname, int onvifPort) {
|
||||
ThingTypeUID thingtypeuid = new ThingTypeUID("ipcamera", brand);
|
||||
ThingUID thingUID = new ThingUID(thingtypeuid, hostname.replace(".", ""));
|
||||
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
|
||||
.withProperty(CONFIG_IPADDRESS, hostname).withProperty(CONFIG_ONVIF_PORT, onvifPort)
|
||||
.withLabel(brand + " camera @" + hostname).build();
|
||||
thingDiscovered(discoveryResult);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
removeOlderResults(getTimestampOfLastScan());
|
||||
OnvifDiscovery onvifDiscovery = new OnvifDiscovery(this);
|
||||
try {
|
||||
onvifDiscovery.discoverCameras(3702);// WS discovery
|
||||
onvifDiscovery.discoverCameras(1900);// SSDP
|
||||
} catch (UnknownHostException | InterruptedException e) {
|
||||
logger.warn(
|
||||
"IpCamera Discovery has an issue discovering the network settings to find cameras with. Try setting up the camera manually.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
|
||||
package org.openhab.binding.ipcamera.internal;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraGroupHandler;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.openhab.core.net.NetworkAddressService;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* The {@link IpCameraHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
@Component(service = ThingHandlerFactory.class, immediate = true, configurationPid = "binding.ipcamera")
|
||||
@NonNullByDefault
|
||||
public class IpCameraHandlerFactory extends BaseThingHandlerFactory {
|
||||
private final @Nullable String openhabIpAddress;
|
||||
private final GroupTracker groupTracker = new GroupTracker();
|
||||
|
||||
@Activate
|
||||
public IpCameraHandlerFactory(final @Reference NetworkAddressService networkAddressService) {
|
||||
openhabIpAddress = networkAddressService.getPrimaryIpv4HostAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
if (SUPPORTED_THING_TYPES.contains(thingTypeUID) || GROUP_SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
|
||||
return new IpCameraHandler(thing, openhabIpAddress, groupTracker);
|
||||
} else if (GROUP_SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
|
||||
return new IpCameraGroupHandler(thing, openhabIpAddress, groupTracker);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
|
||||
package org.openhab.binding.ipcamera.internal;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Random;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.http.HttpResponse;
|
||||
|
||||
/**
|
||||
* The {@link MyNettyAuthHandler} is responsible for handling the basic and digest auths
|
||||
*
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class MyNettyAuthHandler extends ChannelDuplexHandler {
|
||||
public final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private IpCameraHandler ipCameraHandler;
|
||||
private String username, password;
|
||||
private String httpMethod = "", httpUrl = "";
|
||||
private byte ncCounter = 0;
|
||||
private String nonce = "", opaque = "", qop = "";
|
||||
private String realm = "";
|
||||
|
||||
public MyNettyAuthHandler(String user, String pass, IpCameraHandler handle) {
|
||||
ipCameraHandler = handle;
|
||||
username = user;
|
||||
password = pass;
|
||||
}
|
||||
|
||||
public void setURL(String method, String url) {
|
||||
httpUrl = url;
|
||||
httpMethod = method;
|
||||
}
|
||||
|
||||
private String calcMD5Hash(String toHash) {
|
||||
try {
|
||||
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
|
||||
byte[] array = messageDigest.digest(toHash.getBytes());
|
||||
StringBuffer stringBuffer = new StringBuffer();
|
||||
for (int i = 0; i < array.length; ++i) {
|
||||
stringBuffer.append(Integer.toHexString((array[i] & 0xFF) | 0x100).substring(1, 3));
|
||||
}
|
||||
return stringBuffer.toString();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
logger.warn("NoSuchAlgorithmException error when calculating MD5 hash");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// Method can be used a few ways. processAuth(null, string,string, false) to return the digest on demand, and
|
||||
// processAuth(challString, string,string, true) to auto send new packet
|
||||
// First run it should not have authenticate as null
|
||||
// nonce is reused if authenticate is null so the NC needs to increment to allow this//
|
||||
public void processAuth(String authenticate, String httpMethod, String requestURI, boolean reSend) {
|
||||
if (authenticate.contains("Basic realm=\"")) {
|
||||
if (ipCameraHandler.useDigestAuth == true) {
|
||||
// Possible downgrade authenticate attack avoided.
|
||||
return;
|
||||
}
|
||||
logger.debug("Setting up the camera to use Basic Auth and resending last request with correct auth.");
|
||||
if (ipCameraHandler.setBasicAuth(true)) {
|
||||
ipCameraHandler.sendHttpRequest(httpMethod, requestURI, null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/////// Fresh Digest Authenticate method follows as Basic is already handled and returned ////////
|
||||
realm = Helper.searchString(authenticate, "realm=\"");
|
||||
if (realm.isEmpty()) {
|
||||
logger.warn("Could not find a valid WWW-Authenticate response in :{}", authenticate);
|
||||
return;
|
||||
}
|
||||
nonce = Helper.searchString(authenticate, "nonce=\"");
|
||||
opaque = Helper.searchString(authenticate, "opaque=\"");
|
||||
qop = Helper.searchString(authenticate, "qop=\"");
|
||||
|
||||
if (!qop.isEmpty() && !realm.isEmpty()) {
|
||||
ipCameraHandler.useDigestAuth = true;
|
||||
} else {
|
||||
logger.warn(
|
||||
"!!!! Something is wrong with the reply back from the camera. WWW-Authenticate header: qop:{}, realm:{}",
|
||||
qop, realm);
|
||||
}
|
||||
|
||||
String stale = Helper.searchString(authenticate, "stale=\"");
|
||||
if (stale.isEmpty()) {
|
||||
} else if (stale.equalsIgnoreCase("true")) {
|
||||
logger.debug("Camera reported stale=true which normally means the NONCE has expired.");
|
||||
}
|
||||
|
||||
if (password.isEmpty()) {
|
||||
ipCameraHandler.cameraConfigError("Camera gave a 401 reply: You need to provide a password.");
|
||||
return;
|
||||
}
|
||||
// create the MD5 hashes
|
||||
String ha1 = username + ":" + realm + ":" + password;
|
||||
ha1 = calcMD5Hash(ha1);
|
||||
Random random = new Random();
|
||||
String cnonce = Integer.toHexString(random.nextInt());
|
||||
ncCounter = (ncCounter > 125) ? 1 : ++ncCounter;
|
||||
String nc = String.format("%08X", ncCounter); // 8 digit hex number
|
||||
String ha2 = httpMethod + ":" + requestURI;
|
||||
ha2 = calcMD5Hash(ha2);
|
||||
|
||||
String response = ha1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + ha2;
|
||||
response = calcMD5Hash(response);
|
||||
|
||||
String digestString = "username=\"" + username + "\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", uri=\""
|
||||
+ requestURI + "\", cnonce=\"" + cnonce + "\", nc=" + nc + ", qop=\"" + qop + "\", response=\""
|
||||
+ response + "\", opaque=\"" + opaque + "\"";
|
||||
|
||||
if (reSend) {
|
||||
ipCameraHandler.sendHttpRequest(httpMethod, requestURI, digestString);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
|
||||
if (msg == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
boolean closeConnection = true;
|
||||
String authenticate = "";
|
||||
if (msg instanceof HttpResponse) {
|
||||
HttpResponse response = (HttpResponse) msg;
|
||||
if (response.status().code() == 401) {
|
||||
if (!response.headers().isEmpty()) {
|
||||
for (CharSequence name : response.headers().names()) {
|
||||
for (CharSequence value : response.headers().getAll(name)) {
|
||||
if (name.toString().equalsIgnoreCase("WWW-Authenticate")) {
|
||||
authenticate = value.toString();
|
||||
}
|
||||
if (name.toString().equalsIgnoreCase("Connection")
|
||||
&& value.toString().contains("keep-alive")) {
|
||||
// closeConnection = false;
|
||||
// trial this for a while to see if it solves too many bytes with digest turned on.
|
||||
closeConnection = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!authenticate.isEmpty()) {
|
||||
processAuth(authenticate, httpMethod, httpUrl, true);
|
||||
} else {
|
||||
ipCameraHandler.cameraConfigError(
|
||||
"Camera gave no WWW-Authenticate: Your login details must be wrong.");
|
||||
}
|
||||
if (closeConnection) {
|
||||
ctx.close();// needs to be here
|
||||
}
|
||||
}
|
||||
} else if (response.status().code() != 200) {
|
||||
logger.debug("Camera at IP:{} gave a reply with a response code of :{}",
|
||||
ipCameraHandler.cameraConfig.getIp(), response.status().code());
|
||||
}
|
||||
}
|
||||
// Pass the Message back to the pipeline for the next handler to process//
|
||||
super.channelRead(ctx, msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
|
||||
package org.openhab.binding.ipcamera.internal;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.CHANNEL_START_STREAM;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraGroupHandler;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.handler.codec.http.DefaultHttpResponse;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpHeaderValues;
|
||||
import io.netty.handler.codec.http.HttpMethod;
|
||||
import io.netty.handler.codec.http.HttpRequest;
|
||||
import io.netty.handler.codec.http.HttpResponse;
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import io.netty.handler.codec.http.QueryStringDecoder;
|
||||
import io.netty.handler.stream.ChunkedFile;
|
||||
import io.netty.handler.timeout.IdleState;
|
||||
import io.netty.handler.timeout.IdleStateEvent;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
/**
|
||||
* The {@link StreamServerGroupHandler} class is responsible for handling streams and sending any requested files to
|
||||
* Openhabs
|
||||
* features for a group of cameras instead of individual cameras.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class StreamServerGroupHandler extends ChannelInboundHandlerAdapter {
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private IpCameraGroupHandler ipCameraGroupHandler;
|
||||
private String whiteList = "";
|
||||
|
||||
public StreamServerGroupHandler(IpCameraGroupHandler ipCameraGroupHandler) {
|
||||
this.ipCameraGroupHandler = ipCameraGroupHandler;
|
||||
whiteList = ipCameraGroupHandler.groupConfig.getIpWhitelist();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
|
||||
}
|
||||
|
||||
private String resolveIndexToPath(String uri) {
|
||||
if (!uri.substring(1, 2).equals("i")) {
|
||||
return ipCameraGroupHandler.getOutputFolder(Integer.parseInt(uri.substring(1, 2)));
|
||||
}
|
||||
return "notFound";
|
||||
// example is /1ipcameraxx.ts
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
|
||||
if (msg == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (msg instanceof HttpRequest) {
|
||||
HttpRequest httpRequest = (HttpRequest) msg;
|
||||
String requestIP = "("
|
||||
+ ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress().getHostAddress() + ")";
|
||||
if (!whiteList.contains(requestIP) && !whiteList.equals("DISABLE")) {
|
||||
logger.warn("The request made from {} was not in the whitelist and will be ignored.", requestIP);
|
||||
return;
|
||||
} else if (HttpMethod.GET.equals(httpRequest.method())) {
|
||||
// Some browsers send a query string after the path when refreshing a picture.
|
||||
QueryStringDecoder queryStringDecoder = new QueryStringDecoder(httpRequest.uri());
|
||||
switch (queryStringDecoder.path()) {
|
||||
case "/ipcamera.m3u8":
|
||||
if (ipCameraGroupHandler.hlsTurnedOn) {
|
||||
String debugMe = ipCameraGroupHandler.getPlayList();
|
||||
logger.debug("playlist is:{}", debugMe);
|
||||
sendString(ctx, debugMe, "application/x-mpegurl");
|
||||
return;
|
||||
} else {
|
||||
logger.warn(
|
||||
"HLS requires the groups startStream channel to be turned on first. Just starting it now.");
|
||||
String channelPrefix = "ipcamera:" + ipCameraGroupHandler.getThing().getThingTypeUID()
|
||||
+ ":" + ipCameraGroupHandler.getThing().getUID().getId() + ":";
|
||||
ipCameraGroupHandler.handleCommand(new ChannelUID(channelPrefix + CHANNEL_START_STREAM),
|
||||
OnOffType.ON);
|
||||
}
|
||||
break;
|
||||
case "/ipcamera.jpg":
|
||||
sendSnapshotImage(ctx, "image/jpg");
|
||||
return;
|
||||
default:
|
||||
if (httpRequest.uri().contains(".ts")) {
|
||||
sendFile(ctx, resolveIndexToPath(httpRequest.uri()) + httpRequest.uri().substring(2),
|
||||
"video/MP2T");
|
||||
} else if (httpRequest.uri().contains(".jpg")) {
|
||||
sendFile(ctx, httpRequest.uri(), "image/jpg");
|
||||
} else if (httpRequest.uri().contains(".m4s") || httpRequest.uri().contains(".mp4")) {
|
||||
sendFile(ctx, httpRequest.uri(), "video/mp4");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendSnapshotImage(ChannelHandlerContext ctx, String contentType) {
|
||||
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
|
||||
if (ipCameraGroupHandler.cameraIndex >= ipCameraGroupHandler.cameraOrder.size()) {
|
||||
logger.debug("WARN: Openhab may still be starting, or all cameras in the group are OFFLINE.");
|
||||
return;
|
||||
}
|
||||
IpCameraHandler handler = ipCameraGroupHandler.cameraOrder.get(ipCameraGroupHandler.cameraIndex);
|
||||
handler.lockCurrentSnapshot.lock();
|
||||
try {
|
||||
ByteBuf snapshotData = Unpooled.copiedBuffer(handler.currentSnapshot);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
|
||||
response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
|
||||
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_LENGTH, snapshotData.readableBytes());
|
||||
response.headers().add("Access-Control-Allow-Origin", "*");
|
||||
response.headers().add("Access-Control-Expose-Headers", "*");
|
||||
ctx.channel().write(response);
|
||||
ctx.channel().write(snapshotData);
|
||||
ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
|
||||
ctx.channel().writeAndFlush(footerBbuf);
|
||||
} finally {
|
||||
handler.lockCurrentSnapshot.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void sendFile(ChannelHandlerContext ctx, String fileUri, String contentType) throws IOException {
|
||||
logger.trace("file is :{}", fileUri);
|
||||
File file = new File(fileUri);
|
||||
ChunkedFile chunkedFile = new ChunkedFile(file);
|
||||
ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
|
||||
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
|
||||
response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
|
||||
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_LENGTH, chunkedFile.length());
|
||||
response.headers().add("Access-Control-Allow-Origin", "*");
|
||||
response.headers().add("Access-Control-Expose-Headers", "*");
|
||||
ctx.channel().write(response);
|
||||
ctx.channel().write(chunkedFile);
|
||||
ctx.channel().writeAndFlush(footerBbuf);
|
||||
}
|
||||
|
||||
private void sendString(ChannelHandlerContext ctx, String contents, String contentType) {
|
||||
ByteBuf contentsBbuf = Unpooled.copiedBuffer(contents, 0, contents.length(), StandardCharsets.UTF_8);
|
||||
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
|
||||
response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
|
||||
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_LENGTH, contentsBbuf.readableBytes());
|
||||
response.headers().add("Access-Control-Allow-Origin", "*");
|
||||
response.headers().add("Access-Control-Expose-Headers", "*");
|
||||
ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
|
||||
ctx.channel().write(response);
|
||||
ctx.channel().write(contentsBbuf);
|
||||
ctx.channel().writeAndFlush(footerBbuf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelReadComplete(@Nullable ChannelHandlerContext ctx) throws Exception {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) throws Exception {
|
||||
if (cause == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
if (cause.toString().contains("Connection reset by peer")) {
|
||||
logger.debug("Connection reset by peer.");
|
||||
} else if (cause.toString().contains("An established connection was aborted by the software")) {
|
||||
logger.debug("An established connection was aborted by the software");
|
||||
} else if (cause.toString().contains("An existing connection was forcibly closed by the remote host")) {
|
||||
logger.debug("An existing connection was forcibly closed by the remote host");
|
||||
} else if (cause.toString().contains("(No such file or directory)")) {
|
||||
logger.info(
|
||||
"IpCameras file server could not find the requested file. This may happen if ffmpeg is still creating the file.");
|
||||
} else {
|
||||
logger.warn("Exception caught from stream server:{}", cause.getMessage());
|
||||
}
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
|
||||
if (evt == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
if (evt instanceof IdleStateEvent) {
|
||||
IdleStateEvent e = (IdleStateEvent) evt;
|
||||
if (e.state() == IdleState.WRITER_IDLE) {
|
||||
logger.debug("Stream server is going to close an idle channel.");
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
|
||||
if (ctx == null) {
|
||||
return;
|
||||
}
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
|
||||
package org.openhab.binding.ipcamera.internal;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.FFmpegFormat;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.handler.codec.http.DefaultHttpResponse;
|
||||
import io.netty.handler.codec.http.HttpContent;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpHeaderValues;
|
||||
import io.netty.handler.codec.http.HttpRequest;
|
||||
import io.netty.handler.codec.http.HttpResponse;
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import io.netty.handler.codec.http.LastHttpContent;
|
||||
import io.netty.handler.codec.http.QueryStringDecoder;
|
||||
import io.netty.handler.stream.ChunkedFile;
|
||||
import io.netty.handler.timeout.IdleState;
|
||||
import io.netty.handler.timeout.IdleStateEvent;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
/**
|
||||
* The {@link StreamServerHandler} class is responsible for handling streams and sending any requested files to openHABs
|
||||
* features.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class StreamServerHandler extends ChannelInboundHandlerAdapter {
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private IpCameraHandler ipCameraHandler;
|
||||
private boolean handlingMjpeg = false; // used to remove ctx from group when handler is removed.
|
||||
private boolean handlingSnapshotStream = false; // used to remove ctx from group when handler is removed.
|
||||
private byte[] incomingJpeg = new byte[0];
|
||||
private String whiteList = "";
|
||||
private int recievedBytes = 0;
|
||||
private boolean updateSnapshot = false;
|
||||
private boolean onvifEvent = false;
|
||||
|
||||
public StreamServerHandler(IpCameraHandler ipCameraHandler) {
|
||||
this.ipCameraHandler = ipCameraHandler;
|
||||
whiteList = ipCameraHandler.getWhiteList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
|
||||
if (ctx == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (msg instanceof HttpRequest) {
|
||||
HttpRequest httpRequest = (HttpRequest) msg;
|
||||
if (!whiteList.equals("DISABLE")) {
|
||||
String requestIP = "("
|
||||
+ ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress().getHostAddress() + ")";
|
||||
if (!whiteList.contains(requestIP)) {
|
||||
logger.warn("The request made from {} was not in the whitelist and will be ignored.",
|
||||
requestIP);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if ("GET".equalsIgnoreCase(httpRequest.method().toString())) {
|
||||
logger.debug("Stream Server recieved request \tGET:{}", httpRequest.uri());
|
||||
// Some browsers send a query string after the path when refreshing a picture.
|
||||
QueryStringDecoder queryStringDecoder = new QueryStringDecoder(httpRequest.uri());
|
||||
switch (queryStringDecoder.path()) {
|
||||
case "/ipcamera.m3u8":
|
||||
if (ipCameraHandler.ffmpegHLS != null) {
|
||||
if (!ipCameraHandler.ffmpegHLS.getIsAlive()) {
|
||||
if (ipCameraHandler.ffmpegHLS != null) {
|
||||
ipCameraHandler.ffmpegHLS.startConverting();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ipCameraHandler.setupFfmpegFormat(FFmpegFormat.HLS);
|
||||
}
|
||||
if (ipCameraHandler.ffmpegHLS != null) {
|
||||
ipCameraHandler.ffmpegHLS.setKeepAlive(8);
|
||||
}
|
||||
sendFile(ctx, httpRequest.uri(), "application/x-mpegurl");
|
||||
ctx.close();
|
||||
return;
|
||||
case "/ipcamera.mpd":
|
||||
sendFile(ctx, httpRequest.uri(), "application/dash+xml");
|
||||
return;
|
||||
case "/ipcamera.gif":
|
||||
sendFile(ctx, httpRequest.uri(), "image/gif");
|
||||
return;
|
||||
case "/ipcamera.jpg":
|
||||
if (!ipCameraHandler.snapshotPolling && ipCameraHandler.snapshotUri != "") {
|
||||
ipCameraHandler.sendHttpGET(ipCameraHandler.snapshotUri);
|
||||
}
|
||||
if (ipCameraHandler.currentSnapshot.length == 1) {
|
||||
logger.warn("ipcamera.jpg was requested but there is no jpg in ram to send.");
|
||||
return;
|
||||
}
|
||||
sendSnapshotImage(ctx, "image/jpg");
|
||||
return;
|
||||
case "/snapshots.mjpeg":
|
||||
handlingSnapshotStream = true;
|
||||
ipCameraHandler.startSnapshotPolling();
|
||||
ipCameraHandler.setupSnapshotStreaming(true, ctx, false);
|
||||
return;
|
||||
case "/ipcamera.mjpeg":
|
||||
ipCameraHandler.setupMjpegStreaming(true, ctx);
|
||||
handlingMjpeg = true;
|
||||
return;
|
||||
case "/autofps.mjpeg":
|
||||
handlingSnapshotStream = true;
|
||||
ipCameraHandler.setupSnapshotStreaming(true, ctx, true);
|
||||
return;
|
||||
case "/instar":
|
||||
InstarHandler instar = new InstarHandler(ipCameraHandler);
|
||||
instar.alarmTriggered(httpRequest.uri().toString());
|
||||
ctx.close();
|
||||
return;
|
||||
case "/ipcamera0.ts":
|
||||
default:
|
||||
if (httpRequest.uri().contains(".ts")) {
|
||||
sendFile(ctx, queryStringDecoder.path(), "video/MP2T");
|
||||
} else if (httpRequest.uri().contains(".gif")) {
|
||||
sendFile(ctx, queryStringDecoder.path(), "image/gif");
|
||||
} else if (httpRequest.uri().contains(".jpg")) {
|
||||
// Allow access to the preroll and postroll jpg files
|
||||
sendFile(ctx, queryStringDecoder.path(), "image/jpg");
|
||||
} else if (httpRequest.uri().contains(".m4s") || httpRequest.uri().contains(".mp4")) {
|
||||
sendFile(ctx, queryStringDecoder.path(), "video/mp4");
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if ("POST".equalsIgnoreCase(httpRequest.method().toString())) {
|
||||
switch (httpRequest.uri()) {
|
||||
case "/ipcamera.jpg":
|
||||
break;
|
||||
case "/snapshot.jpg":
|
||||
updateSnapshot = true;
|
||||
break;
|
||||
case "/OnvifEvent":
|
||||
onvifEvent = true;
|
||||
break;
|
||||
default:
|
||||
logger.debug("Stream Server recieved unknown request \tPOST:{}", httpRequest.uri());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (msg instanceof HttpContent) {
|
||||
HttpContent content = (HttpContent) msg;
|
||||
int index = 0;
|
||||
if (recievedBytes == 0) {
|
||||
incomingJpeg = new byte[content.content().capacity()];
|
||||
} else {
|
||||
byte[] temp = incomingJpeg;
|
||||
incomingJpeg = new byte[recievedBytes + content.content().capacity()];
|
||||
|
||||
for (; index < temp.length; index++) {
|
||||
incomingJpeg[index] = temp[index];
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < content.content().capacity(); i++) {
|
||||
incomingJpeg[index++] = content.content().getByte(i);
|
||||
}
|
||||
recievedBytes = incomingJpeg.length;
|
||||
if (content instanceof LastHttpContent) {
|
||||
if (updateSnapshot) {
|
||||
ipCameraHandler.processSnapshot(incomingJpeg);
|
||||
} else if (onvifEvent) {
|
||||
ipCameraHandler.onvifCamera.eventRecieved(new String(incomingJpeg, StandardCharsets.UTF_8));
|
||||
} else { // handles the snapshots that make up mjpeg from rtsp to ffmpeg conversions.
|
||||
if (recievedBytes > 1000) {
|
||||
ipCameraHandler.sendMjpegFrame(incomingJpeg, ipCameraHandler.mjpegChannelGroup);
|
||||
}
|
||||
}
|
||||
recievedBytes = 0;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendSnapshotImage(ChannelHandlerContext ctx, String contentType) {
|
||||
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
|
||||
ipCameraHandler.lockCurrentSnapshot.lock();
|
||||
try {
|
||||
ByteBuf snapshotData = Unpooled.copiedBuffer(ipCameraHandler.currentSnapshot);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
|
||||
response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
|
||||
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_LENGTH, snapshotData.readableBytes());
|
||||
response.headers().add("Access-Control-Allow-Origin", "*");
|
||||
response.headers().add("Access-Control-Expose-Headers", "*");
|
||||
ctx.channel().write(response);
|
||||
ctx.channel().write(snapshotData);
|
||||
ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
|
||||
ctx.channel().writeAndFlush(footerBbuf);
|
||||
} finally {
|
||||
ipCameraHandler.lockCurrentSnapshot.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void sendFile(ChannelHandlerContext ctx, String fileUri, String contentType) throws IOException {
|
||||
File file = new File(ipCameraHandler.cameraConfig.getFfmpegOutput() + fileUri);
|
||||
ChunkedFile chunkedFile = new ChunkedFile(file);
|
||||
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType);
|
||||
response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE);
|
||||
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
|
||||
response.headers().add(HttpHeaderNames.CONTENT_LENGTH, chunkedFile.length());
|
||||
response.headers().add("Access-Control-Allow-Origin", "*");
|
||||
response.headers().add("Access-Control-Expose-Headers", "*");
|
||||
ctx.channel().write(response);
|
||||
ctx.channel().write(chunkedFile);
|
||||
ByteBuf footerBbuf = Unpooled.copiedBuffer("\r\n", 0, 2, StandardCharsets.UTF_8);
|
||||
ctx.channel().writeAndFlush(footerBbuf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelReadComplete(@Nullable ChannelHandlerContext ctx) throws Exception {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) throws Exception {
|
||||
if (ctx == null || cause == null) {
|
||||
return;
|
||||
}
|
||||
if (cause.toString().contains("Connection reset by peer")) {
|
||||
logger.trace("Connection reset by peer.");
|
||||
} else if (cause.toString().contains("An established connection was aborted by the software")) {
|
||||
logger.debug("An established connection was aborted by the software");
|
||||
} else if (cause.toString().contains("An existing connection was forcibly closed by the remote host")) {
|
||||
logger.debug("An existing connection was forcibly closed by the remote host");
|
||||
} else if (cause.toString().contains("(No such file or directory)")) {
|
||||
logger.info(
|
||||
"IpCameras file server could not find the requested file. This may happen if ffmpeg is still creating the file.");
|
||||
} else {
|
||||
logger.warn("Exception caught from stream server:{}", cause.getMessage());
|
||||
}
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
|
||||
if (ctx == null) {
|
||||
return;
|
||||
}
|
||||
if (evt instanceof IdleStateEvent) {
|
||||
IdleStateEvent e = (IdleStateEvent) evt;
|
||||
if (e.state() == IdleState.WRITER_IDLE) {
|
||||
logger.debug("Stream server is going to close an idle channel.");
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
|
||||
if (ctx == null) {
|
||||
return;
|
||||
}
|
||||
ctx.close();
|
||||
if (handlingMjpeg) {
|
||||
ipCameraHandler.setupMjpegStreaming(false, ctx);
|
||||
} else if (handlingSnapshotStream) {
|
||||
handlingSnapshotStream = false;
|
||||
ipCameraHandler.setupSnapshotStreaming(false, ctx, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
|
||||
package org.openhab.binding.ipcamera.internal.handler;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.GroupConfig;
|
||||
import org.openhab.binding.ipcamera.internal.GroupTracker;
|
||||
import org.openhab.binding.ipcamera.internal.Helper;
|
||||
import org.openhab.binding.ipcamera.internal.StreamServerGroupHandler;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
import io.netty.handler.codec.http.HttpServerCodec;
|
||||
import io.netty.handler.stream.ChunkedWriteHandler;
|
||||
import io.netty.handler.timeout.IdleStateHandler;
|
||||
|
||||
/**
|
||||
* The {@link IpCameraGroupHandler} is responsible for finding cameras that are part of this group and displaying a
|
||||
* group picture.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class IpCameraGroupHandler extends BaseThingHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
public GroupConfig groupConfig;
|
||||
private BigDecimal pollTimeInSeconds = new BigDecimal(2);
|
||||
public ArrayList<IpCameraHandler> cameraOrder = new ArrayList<IpCameraHandler>(2);
|
||||
private EventLoopGroup serversLoopGroup = new NioEventLoopGroup();
|
||||
private final ScheduledExecutorService pollCameraGroup = Executors.newSingleThreadScheduledExecutor();
|
||||
private @Nullable ScheduledFuture<?> pollCameraGroupJob = null;
|
||||
private @Nullable ServerBootstrap serverBootstrap;
|
||||
private @Nullable ChannelFuture serverFuture = null;
|
||||
public String hostIp;
|
||||
private boolean motionChangesOrder = true;
|
||||
public int serverPort = 0;
|
||||
public String playList = "";
|
||||
private String playingNow = "";
|
||||
public int cameraIndex = 0;
|
||||
public boolean hlsTurnedOn = false;
|
||||
private int entries = 0;
|
||||
private int mediaSequence = 1;
|
||||
private int discontinuitySequence = 0;
|
||||
private GroupTracker groupTracker;
|
||||
|
||||
public IpCameraGroupHandler(Thing thing, @Nullable String openhabIpAddress, GroupTracker groupTracker) {
|
||||
super(thing);
|
||||
groupConfig = getConfigAs(GroupConfig.class);
|
||||
if (openhabIpAddress != null) {
|
||||
hostIp = openhabIpAddress;
|
||||
} else {
|
||||
hostIp = Helper.getLocalIpAddress();
|
||||
}
|
||||
this.groupTracker = groupTracker;
|
||||
}
|
||||
|
||||
public String getPlayList() {
|
||||
return playList;
|
||||
}
|
||||
|
||||
public String getOutputFolder(int index) {
|
||||
IpCameraHandler handle = cameraOrder.get(index);
|
||||
return handle.cameraConfig.getFfmpegOutput();
|
||||
}
|
||||
|
||||
private String readCamerasPlaylist(int cameraIndex) {
|
||||
String camerasm3u8 = "";
|
||||
IpCameraHandler handle = cameraOrder.get(cameraIndex);
|
||||
try {
|
||||
String file = handle.cameraConfig.getFfmpegOutput() + "ipcamera.m3u8";
|
||||
camerasm3u8 = new String(Files.readAllBytes(Paths.get(file)));
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error occured fetching a groupDisplay cameras m3u8 file :{}", e.getMessage());
|
||||
}
|
||||
return camerasm3u8;
|
||||
}
|
||||
|
||||
String keepLast(String string, int numberToRetain) {
|
||||
int start = string.length();
|
||||
for (int loop = numberToRetain; loop > 0; loop--) {
|
||||
start = string.lastIndexOf("#EXTINF:", start - 1);
|
||||
if (start == -1) {
|
||||
logger.warn(
|
||||
"Playlist did not contain enough entries, check all cameras in groups use the same HLS settings.");
|
||||
return "";
|
||||
}
|
||||
}
|
||||
entries = entries + numberToRetain;
|
||||
return string.substring(start);
|
||||
}
|
||||
|
||||
String removeFromStart(String string, int numberToRemove) {
|
||||
int startingFrom = string.indexOf("#EXTINF:");
|
||||
for (int loop = numberToRemove; loop > 0; loop--) {
|
||||
startingFrom = string.indexOf("#EXTINF:", startingFrom + 27);
|
||||
if (startingFrom == -1) {
|
||||
logger.warn(
|
||||
"Playlist failed to remove entries from start, check all cameras in groups use the same HLS settings.");
|
||||
return string;
|
||||
}
|
||||
}
|
||||
mediaSequence = mediaSequence + numberToRemove;
|
||||
entries = entries - numberToRemove;
|
||||
return string.substring(startingFrom);
|
||||
}
|
||||
|
||||
int howManySegments(String m3u8File) {
|
||||
int start = m3u8File.length();
|
||||
int numberOfFiles = 0;
|
||||
for (BigDecimal totalTime = new BigDecimal(0); totalTime.intValue() < pollTimeInSeconds
|
||||
.intValue(); numberOfFiles++) {
|
||||
start = m3u8File.lastIndexOf("#EXTINF:", start - 1);
|
||||
if (start != -1) {
|
||||
totalTime = totalTime.add(new BigDecimal(m3u8File.substring(start + 8, m3u8File.indexOf(",", start))));
|
||||
} else {
|
||||
logger.debug("Group did not find enough segments, lower the poll time if this message continues.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
return numberOfFiles;
|
||||
}
|
||||
|
||||
public void createPlayList() {
|
||||
String m3u8File = readCamerasPlaylist(cameraIndex);
|
||||
if (m3u8File == "") {
|
||||
return;
|
||||
}
|
||||
int numberOfSegments = howManySegments(m3u8File);
|
||||
logger.debug("Using {} segmented files to make up a poll period.", numberOfSegments);
|
||||
m3u8File = keepLast(m3u8File, numberOfSegments);
|
||||
m3u8File = m3u8File.replace("ipcamera", cameraIndex + "ipcamera"); // add index so we can then fetch output path
|
||||
if (entries > numberOfSegments * 3) {
|
||||
playingNow = removeFromStart(playingNow, entries - (numberOfSegments * 3));
|
||||
}
|
||||
playingNow = playingNow + "#EXT-X-DISCONTINUITY\n" + m3u8File;
|
||||
playList = "#EXTM3U\n#EXT-X-VERSION:6\n#EXT-X-TARGETDURATION:5\n#EXT-X-ALLOW-CACHE:NO\n#EXT-X-DISCONTINUITY-SEQUENCE:"
|
||||
+ discontinuitySequence + "\n#EXT-X-MEDIA-SEQUENCE:" + mediaSequence + "\n" + playingNow;
|
||||
}
|
||||
|
||||
private IpCameraGroupHandler getHandle() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
public void startStreamServer(boolean start) {
|
||||
if (!start) {
|
||||
serversLoopGroup.shutdownGracefully(8, 8, TimeUnit.SECONDS);
|
||||
serverBootstrap = null;
|
||||
} else {
|
||||
if (serverBootstrap == null) {
|
||||
try {
|
||||
serversLoopGroup = new NioEventLoopGroup();
|
||||
serverBootstrap = new ServerBootstrap();
|
||||
serverBootstrap.group(serversLoopGroup);
|
||||
serverBootstrap.channel(NioServerSocketChannel.class);
|
||||
// IP "0.0.0.0" will bind the server to all network connections//
|
||||
serverBootstrap.localAddress(new InetSocketAddress("0.0.0.0", serverPort));
|
||||
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
protected void initChannel(SocketChannel socketChannel) throws Exception {
|
||||
socketChannel.pipeline().addLast("idleStateHandler", new IdleStateHandler(0, 25, 0));
|
||||
socketChannel.pipeline().addLast("HttpServerCodec", new HttpServerCodec());
|
||||
socketChannel.pipeline().addLast("ChunkedWriteHandler", new ChunkedWriteHandler());
|
||||
socketChannel.pipeline().addLast("streamServerHandler",
|
||||
new StreamServerGroupHandler(getHandle()));
|
||||
}
|
||||
});
|
||||
serverFuture = serverBootstrap.bind().sync();
|
||||
serverFuture.await(4000);
|
||||
logger.info("IpCamera file server for a group of cameras has started on port {} for all NIC's.",
|
||||
serverPort);
|
||||
updateState(CHANNEL_MJPEG_URL,
|
||||
new StringType("http://" + hostIp + ":" + serverPort + "/ipcamera.mjpeg"));
|
||||
updateState(CHANNEL_HLS_URL,
|
||||
new StringType("http://" + hostIp + ":" + serverPort + "/ipcamera.m3u8"));
|
||||
updateState(CHANNEL_IMAGE_URL,
|
||||
new StringType("http://" + hostIp + ":" + serverPort + "/ipcamera.jpg"));
|
||||
} catch (Exception e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Exception occured when starting the streaming server. Try changing the serverPort to another number.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void addCamera(String UniqueID) {
|
||||
if (groupTracker.listOfOnlineCameraUID.contains(UniqueID)) {
|
||||
for (IpCameraHandler handler : groupTracker.listOfOnlineCameraHandlers) {
|
||||
if (handler.getThing().getUID().getId().equals(UniqueID)) {
|
||||
if (!cameraOrder.contains(handler)) {
|
||||
logger.info("Adding {} to a camera group.", UniqueID);
|
||||
if (hlsTurnedOn) {
|
||||
logger.info("Starting HLS for the new camera.");
|
||||
String channelPrefix = "ipcamera:" + handler.getThing().getThingTypeUID() + ":"
|
||||
+ handler.getThing().getUID().getId() + ":";
|
||||
handler.handleCommand(new ChannelUID(channelPrefix + CHANNEL_START_STREAM), OnOffType.ON);
|
||||
}
|
||||
cameraOrder.add(handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Event based. This is called as each camera comes online after the group handler is registered.
|
||||
public void cameraOnline(String uid) {
|
||||
logger.debug("New camera {} came online, checking if part of this group", uid);
|
||||
if (groupConfig.getFirstCamera().equals(uid)) {
|
||||
addCamera(uid);
|
||||
} else if (groupConfig.getSecondCamera().equals(uid)) {
|
||||
addCamera(uid);
|
||||
} else if (groupConfig.getThirdCamera().equals(uid)) {
|
||||
addCamera(uid);
|
||||
} else if (groupConfig.getForthCamera().equals(uid)) {
|
||||
addCamera(uid);
|
||||
}
|
||||
}
|
||||
|
||||
// Event based. This is called as each camera comes online after the group handler is registered.
|
||||
public void cameraOffline(IpCameraHandler handle) {
|
||||
if (cameraOrder.remove(handle)) {
|
||||
logger.info("Camera {} went offline and was removed from a group.", handle.getThing().getUID().getId());
|
||||
}
|
||||
}
|
||||
|
||||
boolean addIfOnline(String UniqueID) {
|
||||
if (groupTracker.listOfOnlineCameraUID.contains(UniqueID)) {
|
||||
addCamera(UniqueID);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void createCameraOrder() {
|
||||
addIfOnline(groupConfig.getFirstCamera());
|
||||
addIfOnline(groupConfig.getSecondCamera());
|
||||
if (!groupConfig.getThirdCamera().isEmpty()) {
|
||||
addIfOnline(groupConfig.getThirdCamera());
|
||||
}
|
||||
if (!groupConfig.getForthCamera().isEmpty()) {
|
||||
addIfOnline(groupConfig.getForthCamera());
|
||||
}
|
||||
// Cameras can now send events of when they go on and offline.
|
||||
groupTracker.listOfGroupHandlers.add(this);
|
||||
}
|
||||
|
||||
int checkForMotion(int nextCamerasIndex) {
|
||||
int checked = 0;
|
||||
for (int index = nextCamerasIndex; checked < cameraOrder.size(); checked++) {
|
||||
if (cameraOrder.get(index).motionDetected) {
|
||||
return index;
|
||||
}
|
||||
if (++index >= cameraOrder.size()) {
|
||||
index = 0;
|
||||
}
|
||||
}
|
||||
return nextCamerasIndex;
|
||||
}
|
||||
|
||||
void pollCameraGroup() {
|
||||
if (cameraOrder.isEmpty()) {
|
||||
createCameraOrder();
|
||||
}
|
||||
if (++cameraIndex >= cameraOrder.size()) {
|
||||
cameraIndex = 0;
|
||||
}
|
||||
if (motionChangesOrder) {
|
||||
cameraIndex = checkForMotion(cameraIndex);
|
||||
}
|
||||
if (hlsTurnedOn) {
|
||||
discontinuitySequence++;
|
||||
createPlayList();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (!(command instanceof RefreshType)) {
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_START_STREAM:
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
hlsTurnedOn = true;
|
||||
for (IpCameraHandler handler : cameraOrder) {
|
||||
String channelPrefix = "ipcamera:" + handler.getThing().getThingTypeUID() + ":"
|
||||
+ handler.getThing().getUID().getId() + ":";
|
||||
|
||||
handler.handleCommand(new ChannelUID(channelPrefix + CHANNEL_START_STREAM), OnOffType.ON);
|
||||
}
|
||||
} else {
|
||||
// TODO: Do we turn all controls OFF or do we remember the state before we turned them all on?
|
||||
hlsTurnedOn = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
groupConfig = getConfigAs(GroupConfig.class);
|
||||
serverPort = groupConfig.getServerPort();
|
||||
pollTimeInSeconds = new BigDecimal(groupConfig.getPollTime());
|
||||
pollTimeInSeconds = pollTimeInSeconds.divide(new BigDecimal(1000), 1, RoundingMode.HALF_UP);
|
||||
motionChangesOrder = groupConfig.getMotionChangesOrder();
|
||||
|
||||
if (serverPort == -1) {
|
||||
logger.warn("The serverPort = -1 which disables a lot of features. See readme for more info.");
|
||||
} else if (serverPort < 1025) {
|
||||
logger.warn("The serverPort is <= 1024 and may cause permission errors under Linux, try a higher port.");
|
||||
}
|
||||
if (groupConfig.getServerPort() > 0) {
|
||||
startStreamServer(true);
|
||||
}
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
pollCameraGroupJob = pollCameraGroup.scheduleAtFixedRate(this::pollCameraGroup, 10000,
|
||||
groupConfig.getPollTime(), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
startStreamServer(false);
|
||||
groupTracker.listOfGroupHandlers.remove(this);
|
||||
if (pollCameraGroupJob != null) {
|
||||
pollCameraGroupJob.cancel(true);
|
||||
pollCameraGroupJob = null;
|
||||
}
|
||||
cameraOrder.clear();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
|
||||
package org.openhab.binding.ipcamera.internal.onvif;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.http.HttpContent;
|
||||
import io.netty.handler.codec.http.LastHttpContent;
|
||||
import io.netty.util.CharsetUtil;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
/**
|
||||
* The {@link OnvifCodec} is used by Netty to decode Onvif traffic into message Strings.
|
||||
*
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class OnvifCodec extends ChannelDuplexHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private String incomingMessage = "";
|
||||
private OnvifConnection onvifConnection;
|
||||
|
||||
OnvifCodec(OnvifConnection onvifConnection) {
|
||||
this.onvifConnection = onvifConnection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
|
||||
if (msg == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (msg instanceof HttpContent) {
|
||||
HttpContent content = (HttpContent) msg;
|
||||
incomingMessage += content.content().toString(CharsetUtil.UTF_8);
|
||||
}
|
||||
if (msg instanceof LastHttpContent) {
|
||||
onvifConnection.processReply(incomingMessage);
|
||||
ctx.close();
|
||||
}
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
|
||||
if (ctx == null || cause == null) {
|
||||
return;
|
||||
}
|
||||
logger.debug("Exception on ONVIF connection: {}", cause.getMessage());
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,830 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
|
||||
package org.openhab.binding.ipcamera.internal.onvif;
|
||||
|
||||
import static org.openhab.binding.ipcamera.internal.IpCameraBindingConstants.*;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Random;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.Helper;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||
import io.netty.handler.codec.http.DefaultFullHttpRequest;
|
||||
import io.netty.handler.codec.http.FullHttpRequest;
|
||||
import io.netty.handler.codec.http.HttpClientCodec;
|
||||
import io.netty.handler.codec.http.HttpHeaderValues;
|
||||
import io.netty.handler.codec.http.HttpMethod;
|
||||
import io.netty.handler.codec.http.HttpRequest;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import io.netty.handler.timeout.IdleStateHandler;
|
||||
|
||||
/**
|
||||
* The {@link OnvifConnection} This is a basic Netty implementation for connecting and communicating to ONVIF cameras.
|
||||
*
|
||||
*
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class OnvifConnection {
|
||||
public static enum RequestType {
|
||||
AbsoluteMove,
|
||||
AddPTZConfiguration,
|
||||
ContinuousMoveLeft,
|
||||
ContinuousMoveRight,
|
||||
ContinuousMoveUp,
|
||||
ContinuousMoveDown,
|
||||
Stop,
|
||||
ContinuousMoveIn,
|
||||
ContinuousMoveOut,
|
||||
CreatePullPointSubscription,
|
||||
GetCapabilities,
|
||||
GetDeviceInformation,
|
||||
GetProfiles,
|
||||
GetServiceCapabilities,
|
||||
GetSnapshotUri,
|
||||
GetStreamUri,
|
||||
GetSystemDateAndTime,
|
||||
Subscribe,
|
||||
Unsubscribe,
|
||||
PullMessages,
|
||||
GetEventProperties,
|
||||
RelativeMoveLeft,
|
||||
RelativeMoveRight,
|
||||
RelativeMoveUp,
|
||||
RelativeMoveDown,
|
||||
RelativeMoveIn,
|
||||
RelativeMoveOut,
|
||||
Renew,
|
||||
GetConfigurations,
|
||||
GetConfigurationOptions,
|
||||
GetConfiguration,
|
||||
SetConfiguration,
|
||||
GetNodes,
|
||||
GetStatus,
|
||||
GotoPreset,
|
||||
GetPresets
|
||||
}
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private @Nullable Bootstrap bootstrap;
|
||||
private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup();
|
||||
private String ipAddress = "";
|
||||
private String user = "";
|
||||
private String password = "";
|
||||
private int onvifPort = 80;
|
||||
private String deviceXAddr = "/onvif/device_service";
|
||||
private String eventXAddr = "/onvif/device_service";
|
||||
private String mediaXAddr = "/onvif/device_service";
|
||||
@SuppressWarnings("unused")
|
||||
private String imagingXAddr = "/onvif/device_service";
|
||||
private String ptzXAddr = "/onvif/ptz_service";
|
||||
private String subscriptionXAddr = "/onvif/device_service";
|
||||
private boolean isConnected = false;
|
||||
private int mediaProfileIndex = 0;
|
||||
private String snapshotUri = "";
|
||||
private String rtspUri = "";
|
||||
private IpCameraHandler ipCameraHandler;
|
||||
private boolean usingEvents = false;
|
||||
|
||||
// These hold the cameras PTZ position in the range that the camera uses, ie
|
||||
// mine is -1 to +1
|
||||
private Float panRangeMin = -1.0f;
|
||||
private Float panRangeMax = 1.0f;
|
||||
private Float tiltRangeMin = -1.0f;
|
||||
private Float tiltRangeMax = 1.0f;
|
||||
private Float zoomMin = 0.0f;
|
||||
private Float zoomMax = 1.0f;
|
||||
// These hold the PTZ values for updating Openhabs controls in 0-100 range
|
||||
private Float currentPanPercentage = 0.0f;
|
||||
private Float currentTiltPercentage = 0.0f;
|
||||
private Float currentZoomPercentage = 0.0f;
|
||||
private Float currentPanCamValue = 0.0f;
|
||||
private Float currentTiltCamValue = 0.0f;
|
||||
private Float currentZoomCamValue = 0.0f;
|
||||
private String ptzNodeToken = "000";
|
||||
private String ptzConfigToken = "000";
|
||||
private int presetTokenIndex = 0;
|
||||
private LinkedList<String> presetTokens = new LinkedList<>();
|
||||
private LinkedList<String> mediaProfileTokens = new LinkedList<>();
|
||||
private boolean ptzDevice = true;
|
||||
|
||||
public OnvifConnection(IpCameraHandler ipCameraHandler, String ipAddress, String user, String password) {
|
||||
this.ipCameraHandler = ipCameraHandler;
|
||||
if (!ipAddress.isEmpty()) {
|
||||
this.user = user;
|
||||
this.password = password;
|
||||
getIPandPortFromUrl(ipAddress);
|
||||
}
|
||||
}
|
||||
|
||||
String getXml(RequestType requestType) {
|
||||
switch (requestType) {
|
||||
case AbsoluteMove:
|
||||
return "<AbsoluteMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken><Position><PanTilt x=\""
|
||||
+ currentPanCamValue + "\" y=\"" + currentTiltCamValue
|
||||
+ "\" space=\"http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace\">\n"
|
||||
+ "</PanTilt>\n" + "<Zoom x=\"" + currentZoomCamValue
|
||||
+ "\" space=\"http://www.onvif.org/ver10/tptz/ZoomSpaces/PositionGenericSpace\">\n"
|
||||
+ "</Zoom>\n" + "</Position>\n"
|
||||
+ "<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"
|
||||
+ "</Speed></AbsoluteMove>";
|
||||
case AddPTZConfiguration: // not tested to work yet
|
||||
return "<AddPTZConfiguration xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken><ConfigurationToken>"
|
||||
+ ptzConfigToken + "</ConfigurationToken></AddPTZConfiguration>";
|
||||
case ContinuousMoveLeft:
|
||||
return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex)
|
||||
+ "</ProfileToken><Velocity><PanTilt x=\"-0.5\" y=\"0\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
|
||||
case ContinuousMoveRight:
|
||||
return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex)
|
||||
+ "</ProfileToken><Velocity><PanTilt x=\"0.5\" y=\"0\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
|
||||
case ContinuousMoveUp:
|
||||
return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex)
|
||||
+ "</ProfileToken><Velocity><PanTilt x=\"0\" y=\"-0.5\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
|
||||
case ContinuousMoveDown:
|
||||
return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex)
|
||||
+ "</ProfileToken><Velocity><PanTilt x=\"0\" y=\"0.5\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
|
||||
case Stop:
|
||||
return "<Stop xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex)
|
||||
+ "</ProfileToken><PanTilt>true</PanTilt><Zoom>true</Zoom></Stop>";
|
||||
case ContinuousMoveIn:
|
||||
return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex)
|
||||
+ "</ProfileToken><Velocity><Zoom x=\"0.5\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
|
||||
case ContinuousMoveOut:
|
||||
return "<ContinuousMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex)
|
||||
+ "</ProfileToken><Velocity><Zoom x=\"-0.5\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Velocity></ContinuousMove>";
|
||||
case CreatePullPointSubscription:
|
||||
return "<CreatePullPointSubscription xmlns=\"http://www.onvif.org/ver10/events/wsdl\"><InitialTerminationTime>PT600S</InitialTerminationTime></CreatePullPointSubscription>";
|
||||
case GetCapabilities:
|
||||
return "<GetCapabilities xmlns=\"http://www.onvif.org/ver10/device/wsdl\"><Category>All</Category></GetCapabilities>";
|
||||
|
||||
case GetDeviceInformation:
|
||||
return "<GetDeviceInformation xmlns=\"http://www.onvif.org/ver10/device/wsdl\"/>";
|
||||
case GetProfiles:
|
||||
return "<GetProfiles xmlns=\"http://www.onvif.org/ver10/media/wsdl\"/>";
|
||||
case GetServiceCapabilities:
|
||||
return "<GetServiceCapabilities xmlns=\"http://docs.oasis-open.org/wsn/b-2/\"></GetServiceCapabilities>";
|
||||
case GetSnapshotUri:
|
||||
return "<GetSnapshotUri xmlns=\"http://www.onvif.org/ver10/media/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetSnapshotUri>";
|
||||
case GetStreamUri:
|
||||
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>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetStreamUri>";
|
||||
case GetSystemDateAndTime:
|
||||
return "<GetSystemDateAndTime xmlns=\"http://www.onvif.org/ver10/device/wsdl\"/>";
|
||||
case Subscribe:
|
||||
return "<Subscribe xmlns=\"http://docs.oasis-open.org/wsn/b-2/\"><ConsumerReference><Address>http://"
|
||||
+ ipCameraHandler.hostIp + ":" + ipCameraHandler.cameraConfig.getServerPort()
|
||||
+ "/OnvifEvent</Address></ConsumerReference></Subscribe>";
|
||||
case Unsubscribe:
|
||||
return "<Unsubscribe xmlns=\"http://docs.oasis-open.org/wsn/b-2/\"></Unsubscribe>";
|
||||
case PullMessages:
|
||||
return "<PullMessages xmlns=\"http://www.onvif.org/ver10/events/wsdl\"><Timeout>PT8S</Timeout><MessageLimit>1</MessageLimit></PullMessages>";
|
||||
case GetEventProperties:
|
||||
return "<GetEventProperties xmlns=\"http://www.onvif.org/ver10/events/wsdl\"/>";
|
||||
case RelativeMoveLeft:
|
||||
return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex)
|
||||
+ "</ProfileToken><Translation><PanTilt x=\"0.05000000\" y=\"0\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
|
||||
case RelativeMoveRight:
|
||||
return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex)
|
||||
+ "</ProfileToken><Translation><PanTilt x=\"-0.05000000\" y=\"0\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
|
||||
case RelativeMoveUp:
|
||||
return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex)
|
||||
+ "</ProfileToken><Translation><PanTilt x=\"0\" y=\"0.100000000\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
|
||||
case RelativeMoveDown:
|
||||
return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex)
|
||||
+ "</ProfileToken><Translation><PanTilt x=\"0\" y=\"-0.100000000\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
|
||||
case RelativeMoveIn:
|
||||
return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex)
|
||||
+ "</ProfileToken><Translation><Zoom x=\"0.0240506344\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
|
||||
case RelativeMoveOut:
|
||||
return "<RelativeMove xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex)
|
||||
+ "</ProfileToken><Translation><Zoom x=\"-0.0240506344\" xmlns=\"http://www.onvif.org/ver10/schema\"/></Translation></RelativeMove>";
|
||||
case Renew:
|
||||
return "<Renew xmlns=\"http://docs.oasis-open.org/wsn/b-2\"><TerminationTime>PT1M</TerminationTime></Renew>";
|
||||
case GetConfigurations:
|
||||
return "<GetConfigurations xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"></GetConfigurations>";
|
||||
case GetConfigurationOptions:
|
||||
return "<GetConfigurationOptions xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ConfigurationToken>"
|
||||
+ ptzConfigToken + "</ConfigurationToken></GetConfigurationOptions>";
|
||||
case GetConfiguration:
|
||||
return "<GetConfiguration xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><PTZConfigurationToken>"
|
||||
+ ptzConfigToken + "</PTZConfigurationToken></GetConfiguration>";
|
||||
case SetConfiguration:// not tested to work yet
|
||||
return "<SetConfiguration xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><PTZConfiguration><NodeToken>"
|
||||
+ ptzNodeToken
|
||||
+ "</NodeToken><DefaultAbsolutePantTiltPositionSpace>AbsolutePanTiltPositionSpace</DefaultAbsolutePantTiltPositionSpace><DefaultAbsoluteZoomPositionSpace>AbsoluteZoomPositionSpace</DefaultAbsoluteZoomPositionSpace></PTZConfiguration></SetConfiguration>";
|
||||
case GetNodes:
|
||||
return "<GetNodes xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"></GetNodes>";
|
||||
case GetStatus:
|
||||
return "<GetStatus xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetStatus>";
|
||||
case GotoPreset:
|
||||
return "<GotoPreset xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken><PresetToken>"
|
||||
+ presetTokens.get(presetTokenIndex)
|
||||
+ "</PresetToken><Speed><PanTilt x=\"0.0\" y=\"0.0\" space=\"\"></PanTilt><Zoom x=\"0.0\" space=\"\"></Zoom></Speed></GotoPreset>";
|
||||
case GetPresets:
|
||||
return "<GetPresets xmlns=\"http://www.onvif.org/ver20/ptz/wsdl\"><ProfileToken>"
|
||||
+ mediaProfileTokens.get(mediaProfileIndex) + "</ProfileToken></GetPresets>";
|
||||
}
|
||||
return "notfound";
|
||||
}
|
||||
|
||||
public void processReply(String message) {
|
||||
logger.trace("Onvif reply is:{}", message);
|
||||
if (message.contains("PullMessagesResponse")) {
|
||||
eventRecieved(message);
|
||||
} else if (message.contains("RenewResponse")) {
|
||||
sendOnvifRequest(requestBuilder(RequestType.PullMessages, subscriptionXAddr));
|
||||
} else if (message.contains("GetSystemDateAndTimeResponse")) {// 1st to be sent.
|
||||
isConnected = true;
|
||||
sendOnvifRequest(requestBuilder(RequestType.GetCapabilities, deviceXAddr));
|
||||
parseDateAndTime(message);
|
||||
logger.debug("Openhabs UTC dateTime is:{}", getUTCdateTime());
|
||||
} else if (message.contains("GetCapabilitiesResponse")) {// 2nd to be sent.
|
||||
parseXAddr(message);
|
||||
sendOnvifRequest(requestBuilder(RequestType.GetProfiles, mediaXAddr));
|
||||
} else if (message.contains("GetProfilesResponse")) {// 3rd to be sent.
|
||||
parseProfiles(message);
|
||||
sendOnvifRequest(requestBuilder(RequestType.GetSnapshotUri, mediaXAddr));
|
||||
sendOnvifRequest(requestBuilder(RequestType.GetStreamUri, mediaXAddr));
|
||||
if (ptzDevice) {
|
||||
sendPTZRequest(RequestType.GetNodes);
|
||||
}
|
||||
if (usingEvents) {// stops API cameras from getting sent ONVIF events.
|
||||
sendOnvifRequest(requestBuilder(RequestType.GetEventProperties, eventXAddr));
|
||||
sendOnvifRequest(requestBuilder(RequestType.GetServiceCapabilities, eventXAddr));
|
||||
}
|
||||
} else if (message.contains("GetServiceCapabilitiesResponse")) {
|
||||
if (message.contains("WSSubscriptionPolicySupport=\"true\"")) {
|
||||
sendOnvifRequest(requestBuilder(RequestType.Subscribe, eventXAddr));
|
||||
}
|
||||
} else if (message.contains("GetEventPropertiesResponse")) {
|
||||
sendOnvifRequest(requestBuilder(RequestType.CreatePullPointSubscription, eventXAddr));
|
||||
} else if (message.contains("SubscribeResponse")) {
|
||||
logger.info("Onvif Subscribe appears to be working for Alarms/Events.");
|
||||
} else if (message.contains("CreatePullPointSubscriptionResponse")) {
|
||||
subscriptionXAddr = removeIPfromUrl(Helper.fetchXML(message, "SubscriptionReference>", "Address>"));
|
||||
logger.debug("subscriptionXAddr={}", subscriptionXAddr);
|
||||
sendOnvifRequest(requestBuilder(RequestType.PullMessages, subscriptionXAddr));
|
||||
} else if (message.contains("GetStatusResponse")) {
|
||||
processPTZLocation(message);
|
||||
} else if (message.contains("GetPresetsResponse")) {
|
||||
presetTokens = listOfResults(message, "<tptz:Preset", "token=\"");
|
||||
} 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 recieved");
|
||||
} else if (message.contains("GetSnapshotUriResponse")) {
|
||||
snapshotUri = removeIPfromUrl(Helper.fetchXML(message, ":MediaUri", ":Uri"));
|
||||
logger.debug("GetSnapshotUri:{}", snapshotUri);
|
||||
if (ipCameraHandler.snapshotUri.isEmpty()) {
|
||||
ipCameraHandler.snapshotUri = snapshotUri;
|
||||
}
|
||||
} else if (message.contains("GetStreamUriResponse")) {
|
||||
rtspUri = Helper.fetchXML(message, ":MediaUri", ":Uri>");
|
||||
logger.debug("GetStreamUri:{}", rtspUri);
|
||||
if (ipCameraHandler.cameraConfig.getFfmpegInput().isEmpty()) {
|
||||
ipCameraHandler.rtspUri = rtspUri;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HttpRequest requestBuilder(RequestType requestType, String xAddr) {
|
||||
logger.trace("Sending ONVIF request:{}", requestType);
|
||||
String security = "";
|
||||
String extraEnvelope = " xmlns:a=\"http://www.w3.org/2005/08/addressing\"";
|
||||
String headerTo = "";
|
||||
String getXmlCache = getXml(requestType);
|
||||
if (requestType.equals(RequestType.CreatePullPointSubscription) || requestType.equals(RequestType.PullMessages)
|
||||
|| requestType.equals(RequestType.Renew) || requestType.equals(RequestType.Unsubscribe)) {
|
||||
headerTo = "<a:To s:mustUnderstand=\"1\">http://" + ipAddress + xAddr + "</a:To>";
|
||||
}
|
||||
if (!password.isEmpty()) {
|
||||
String nonce = createNonce();
|
||||
String dateTime = getUTCdateTime();
|
||||
String digest = createDigest(nonce, dateTime);
|
||||
security = "<Security s:mustUnderstand=\"1\" xmlns=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\"><UsernameToken><Username>"
|
||||
+ user
|
||||
+ "</Username><Password Type=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest\">"
|
||||
+ digest
|
||||
+ "</Password><Nonce EncodingType=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary\">"
|
||||
+ encodeBase64(nonce)
|
||||
+ "</Nonce><Created xmlns=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\">"
|
||||
+ dateTime + "</Created></UsernameToken></Security>";
|
||||
}
|
||||
String headers = "<s:Header>" + security + headerTo + "</s:Header>";
|
||||
|
||||
if (requestType.equals(RequestType.GetSystemDateAndTime)) {
|
||||
extraEnvelope = "";
|
||||
headers = "";
|
||||
}
|
||||
|
||||
FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, new HttpMethod("POST"), xAddr);
|
||||
request.headers().add("Content-Type", "application/soap+xml");
|
||||
request.headers().add("charset", "utf-8");
|
||||
if (onvifPort != 80) {
|
||||
request.headers().set("Host", ipAddress + ":" + onvifPort);
|
||||
} else {
|
||||
request.headers().set("Host", ipAddress);
|
||||
}
|
||||
request.headers().set("Connection", HttpHeaderValues.CLOSE);
|
||||
request.headers().set("Accept-Encoding", "gzip, deflate");
|
||||
String fullXml = "<s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\"" + extraEnvelope + ">"
|
||||
+ headers
|
||||
+ "<s:Body xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">"
|
||||
+ getXmlCache + "</s:Body></s:Envelope>";
|
||||
String actionString = Helper.fetchXML(getXmlCache, requestType.toString(), "xmlns=\"");
|
||||
request.headers().add("SOAPAction", "\"" + actionString + "/" + requestType + "\"");
|
||||
ByteBuf bbuf = Unpooled.copiedBuffer(fullXml, StandardCharsets.UTF_8);
|
||||
request.headers().set("Content-Length", bbuf.readableBytes());
|
||||
request.content().clear().writeBytes(bbuf);
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link removeIPfromUrl} Will throw away all text before the cameras IP, also removes the IP and the PORT
|
||||
* leaving just the
|
||||
* URL.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
String removeIPfromUrl(String url) {
|
||||
int index = url.indexOf(ipAddress);
|
||||
if (index != -1) {// now remove the :port
|
||||
index = url.indexOf("/", index + ipAddress.length());
|
||||
}
|
||||
if (index == -1) {
|
||||
logger.debug("We hit an issue parsing url:{}", url);
|
||||
return "";
|
||||
}
|
||||
return url.substring(index);
|
||||
}
|
||||
|
||||
void parseXAddr(String message) {
|
||||
// Normally I would search '<tt:XAddr>' instead but Foscam needed this work around.
|
||||
String temp = removeIPfromUrl(Helper.fetchXML(message, "<tt:Device", "tt:XAddr"));
|
||||
if (!temp.isEmpty()) {
|
||||
deviceXAddr = temp;
|
||||
logger.debug("deviceXAddr:{}", deviceXAddr);
|
||||
}
|
||||
temp = removeIPfromUrl(Helper.fetchXML(message, "<tt:Events", "tt:XAddr"));
|
||||
if (!temp.isEmpty()) {
|
||||
subscriptionXAddr = eventXAddr = temp;
|
||||
logger.debug("eventsXAddr:{}", eventXAddr);
|
||||
}
|
||||
temp = removeIPfromUrl(Helper.fetchXML(message, "<tt:Media", "tt:XAddr"));
|
||||
if (!temp.isEmpty()) {
|
||||
mediaXAddr = temp;
|
||||
logger.debug("mediaXAddr:{}", mediaXAddr);
|
||||
}
|
||||
|
||||
ptzXAddr = removeIPfromUrl(Helper.fetchXML(message, "<tt:PTZ", "tt:XAddr"));
|
||||
if (ptzXAddr.isEmpty()) {
|
||||
ptzDevice = false;
|
||||
logger.trace("Camera must not support PTZ, it failed to give a <tt:PTZ><tt:XAddr>:{}", message);
|
||||
} else {
|
||||
logger.debug("ptzXAddr:{}", ptzXAddr);
|
||||
}
|
||||
}
|
||||
|
||||
private void parseDateAndTime(String message) {
|
||||
String minute = Helper.fetchXML(message, "UTCDateTime", "Minute>");
|
||||
String hour = Helper.fetchXML(message, "UTCDateTime", "Hour>");
|
||||
String second = Helper.fetchXML(message, "UTCDateTime", "Second>");
|
||||
logger.debug("Cameras UTC time is : {}:{}:{}", hour, minute, second);
|
||||
String day = Helper.fetchXML(message, "UTCDateTime", "Day>");
|
||||
String month = Helper.fetchXML(message, "UTCDateTime", "Month>");
|
||||
String year = Helper.fetchXML(message, "UTCDateTime", "Year>");
|
||||
logger.debug("Cameras UTC date is : {}-{}-{}", year, month, day);
|
||||
}
|
||||
|
||||
private String getUTCdateTime() {
|
||||
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
|
||||
format.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
return format.format(new Date());
|
||||
}
|
||||
|
||||
String createNonce() {
|
||||
Random nonce = new Random();
|
||||
return "" + nonce.nextInt();
|
||||
}
|
||||
|
||||
String encodeBase64(String raw) {
|
||||
return Base64.getEncoder().encodeToString(raw.getBytes());
|
||||
}
|
||||
|
||||
String createDigest(String nOnce, String dateTime) {
|
||||
String beforeEncryption = nOnce + dateTime + password;
|
||||
MessageDigest msgDigest;
|
||||
byte[] encryptedRaw = null;
|
||||
try {
|
||||
msgDigest = MessageDigest.getInstance("SHA-1");
|
||||
msgDigest.reset();
|
||||
msgDigest.update(beforeEncryption.getBytes("utf8"));
|
||||
encryptedRaw = msgDigest.digest();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
}
|
||||
return Base64.getEncoder().encodeToString(encryptedRaw);
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
public void sendOnvifRequest(HttpRequest request) {
|
||||
if (bootstrap == null) {
|
||||
bootstrap = new Bootstrap();
|
||||
bootstrap.group(mainEventLoopGroup);
|
||||
bootstrap.channel(NioSocketChannel.class);
|
||||
bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
|
||||
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
|
||||
bootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
|
||||
bootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
|
||||
bootstrap.option(ChannelOption.TCP_NODELAY, true);
|
||||
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
|
||||
|
||||
@Override
|
||||
public void initChannel(SocketChannel socketChannel) throws Exception {
|
||||
socketChannel.pipeline().addLast("idleStateHandler", new IdleStateHandler(0, 0, 70));
|
||||
socketChannel.pipeline().addLast("HttpClientCodec", new HttpClientCodec());
|
||||
socketChannel.pipeline().addLast("OnvifCodec", new OnvifCodec(getHandle()));
|
||||
}
|
||||
});
|
||||
}
|
||||
bootstrap.connect(new InetSocketAddress(ipAddress, onvifPort)).addListener(new ChannelFutureListener() {
|
||||
|
||||
@Override
|
||||
public void operationComplete(@Nullable ChannelFuture future) {
|
||||
if (future == null) {
|
||||
return;
|
||||
}
|
||||
if (future.isDone() && future.isSuccess()) {
|
||||
Channel ch = future.channel();
|
||||
ch.writeAndFlush(request);
|
||||
} else { // an error occured
|
||||
logger.debug("Camera is not reachable on ONVIF port:{} or the port may be wrong.", onvifPort);
|
||||
if (isConnected) {
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
OnvifConnection getHandle() {
|
||||
return this;
|
||||
}
|
||||
|
||||
void getIPandPortFromUrl(String url) {
|
||||
int beginIndex = url.indexOf(":");
|
||||
int endIndex = url.indexOf("/", beginIndex);
|
||||
if (beginIndex >= 0 && endIndex == -1) {// 192.168.1.1:8080
|
||||
ipAddress = url.substring(0, beginIndex);
|
||||
onvifPort = Integer.parseInt(url.substring(beginIndex + 1));
|
||||
} else if (beginIndex >= 0 && endIndex > beginIndex) {// 192.168.1.1:8080/foo/bar
|
||||
ipAddress = url.substring(0, beginIndex);
|
||||
onvifPort = Integer.parseInt(url.substring(beginIndex + 1, endIndex));
|
||||
} else {// 192.168.1.1
|
||||
ipAddress = url;
|
||||
logger.debug("No Onvif Port found when parsing:{}", url);
|
||||
}
|
||||
}
|
||||
|
||||
public void gotoPreset(int index) {
|
||||
if (ptzDevice) {
|
||||
if (index > 0) {// 0 is reserved for HOME as cameras seem to start at preset 1.
|
||||
if (presetTokens.isEmpty()) {
|
||||
logger.warn("Camera did not report any ONVIF preset locations, updating preset tokens now.");
|
||||
sendPTZRequest(RequestType.GetPresets);
|
||||
} else {
|
||||
presetTokenIndex = index - 1;
|
||||
sendPTZRequest(RequestType.GotoPreset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void eventRecieved(String eventMessage) {
|
||||
String topic = Helper.fetchXML(eventMessage, "Topic", "tns1:");
|
||||
String dataName = Helper.fetchXML(eventMessage, "tt:Data", "Name=\"");
|
||||
String dataValue = Helper.fetchXML(eventMessage, "tt:Data", "Value=\"");
|
||||
if (!topic.isEmpty()) {
|
||||
logger.debug("Onvif Event Topic:{}, Data:{}, Value:{}", topic, dataName, dataValue);
|
||||
}
|
||||
switch (topic) {
|
||||
case "RuleEngine/CellMotionDetector/Motion":
|
||||
if (dataValue.equals("true")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_CELL_MOTION_ALARM);
|
||||
} else if (dataValue.equals("false")) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_CELL_MOTION_ALARM);
|
||||
}
|
||||
break;
|
||||
case "VideoSource/MotionAlarm":
|
||||
if (dataValue.equals("true")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_MOTION_ALARM);
|
||||
} else if (dataValue.equals("false")) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_MOTION_ALARM);
|
||||
}
|
||||
break;
|
||||
case "AudioAnalytics/Audio/DetectedSound":
|
||||
if (dataValue.equals("true")) {
|
||||
ipCameraHandler.audioDetected();
|
||||
} else if (dataValue.equals("false")) {
|
||||
ipCameraHandler.noAudioDetected();
|
||||
}
|
||||
break;
|
||||
case "RuleEngine/FieldDetector/ObjectsInside":
|
||||
if (dataValue.equals("true")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_FIELD_DETECTION_ALARM);
|
||||
} else if (dataValue.equals("false")) {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_FIELD_DETECTION_ALARM);
|
||||
}
|
||||
break;
|
||||
case "RuleEngine/LineDetector/Crossed":
|
||||
if (dataName.equals("ObjectId")) {
|
||||
ipCameraHandler.motionDetected(CHANNEL_LINE_CROSSING_ALARM);
|
||||
} else {
|
||||
ipCameraHandler.noMotionDetected(CHANNEL_LINE_CROSSING_ALARM);
|
||||
}
|
||||
break;
|
||||
case "RuleEngine/TamperDetector/Tamper":
|
||||
if (dataValue.equals("true")) {
|
||||
ipCameraHandler.changeAlarmState(CHANNEL_TAMPER_ALARM, OnOffType.ON);
|
||||
} else if (dataValue.equals("false")) {
|
||||
ipCameraHandler.changeAlarmState(CHANNEL_TAMPER_ALARM, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "Device/HardwareFailure/StorageFailure":
|
||||
if (dataValue.equals("true")) {
|
||||
ipCameraHandler.changeAlarmState(CHANNEL_STORAGE_ALARM, OnOffType.ON);
|
||||
} else if (dataValue.equals("false")) {
|
||||
ipCameraHandler.changeAlarmState(CHANNEL_STORAGE_ALARM, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "VideoSource/ImageTooDark/AnalyticsService":
|
||||
case "VideoSource/ImageTooDark/ImagingService":
|
||||
case "VideoSource/ImageTooDark/RecordingService":
|
||||
if (dataValue.equals("true")) {
|
||||
ipCameraHandler.changeAlarmState(CHANNEL_TOO_DARK_ALARM, OnOffType.ON);
|
||||
} else if (dataValue.equals("false")) {
|
||||
ipCameraHandler.changeAlarmState(CHANNEL_TOO_DARK_ALARM, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "VideoSource/GlobalSceneChange/AnalyticsService":
|
||||
case "VideoSource/GlobalSceneChange/ImagingService":
|
||||
case "VideoSource/GlobalSceneChange/RecordingService":
|
||||
if (dataValue.equals("true")) {
|
||||
ipCameraHandler.changeAlarmState(CHANNEL_SCENE_CHANGE_ALARM, OnOffType.ON);
|
||||
} else if (dataValue.equals("false")) {
|
||||
ipCameraHandler.changeAlarmState(CHANNEL_SCENE_CHANGE_ALARM, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "VideoSource/ImageTooBright/AnalyticsService":
|
||||
case "VideoSource/ImageTooBright/ImagingService":
|
||||
case "VideoSource/ImageTooBright/RecordingService":
|
||||
if (dataValue.equals("true")) {
|
||||
ipCameraHandler.changeAlarmState(CHANNEL_TOO_BRIGHT_ALARM, OnOffType.ON);
|
||||
} else if (dataValue.equals("false")) {
|
||||
ipCameraHandler.changeAlarmState(CHANNEL_TOO_BRIGHT_ALARM, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "VideoSource/ImageTooBlurry/AnalyticsService":
|
||||
case "VideoSource/ImageTooBlurry/ImagingService":
|
||||
case "VideoSource/ImageTooBlurry/RecordingService":
|
||||
if (dataValue.equals("true")) {
|
||||
ipCameraHandler.changeAlarmState(CHANNEL_TOO_BLURRY_ALARM, OnOffType.ON);
|
||||
} else if (dataValue.equals("false")) {
|
||||
ipCameraHandler.changeAlarmState(CHANNEL_TOO_BLURRY_ALARM, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
sendOnvifRequest(requestBuilder(RequestType.Renew, subscriptionXAddr));
|
||||
}
|
||||
|
||||
public boolean supportsPTZ() {
|
||||
return ptzDevice;
|
||||
}
|
||||
|
||||
public void getStatus() {
|
||||
if (ptzDevice) {
|
||||
sendPTZRequest(RequestType.GetStatus);
|
||||
}
|
||||
}
|
||||
|
||||
public Float getAbsolutePan() {
|
||||
return currentPanPercentage;
|
||||
}
|
||||
|
||||
public Float getAbsoluteTilt() {
|
||||
return currentTiltPercentage;
|
||||
}
|
||||
|
||||
public Float getAbsoluteZoom() {
|
||||
return currentZoomPercentage;
|
||||
}
|
||||
|
||||
public void setAbsolutePan(Float panValue) {// Value is 0-100% of cameras range
|
||||
if (ptzDevice) {
|
||||
currentPanPercentage = panValue;
|
||||
currentPanCamValue = ((((panRangeMin - panRangeMax) * -1) / 100) * panValue + panRangeMin);
|
||||
}
|
||||
}
|
||||
|
||||
public void setAbsoluteTilt(Float tiltValue) {// Value is 0-100% of cameras range
|
||||
if (ptzDevice) {
|
||||
currentTiltPercentage = tiltValue;
|
||||
currentTiltCamValue = ((((panRangeMin - panRangeMax) * -1) / 100) * tiltValue + tiltRangeMin);
|
||||
}
|
||||
}
|
||||
|
||||
public void setAbsoluteZoom(Float zoomValue) {// Value is 0-100% of cameras range
|
||||
if (ptzDevice) {
|
||||
currentZoomPercentage = zoomValue;
|
||||
currentZoomCamValue = ((((zoomMin - zoomMax) * -1) / 100) * zoomValue + zoomMin);
|
||||
}
|
||||
}
|
||||
|
||||
public void absoluteMove() { // Camera wont move until PTZ values are set, then call this.
|
||||
if (ptzDevice) {
|
||||
sendPTZRequest(RequestType.AbsoluteMove);
|
||||
}
|
||||
}
|
||||
|
||||
public void setSelectedMediaProfile(int mediaProfileIndex) {
|
||||
this.mediaProfileIndex = mediaProfileIndex;
|
||||
}
|
||||
|
||||
LinkedList<String> listOfResults(String message, String heading, String key) {
|
||||
LinkedList<String> results = new LinkedList<String>();
|
||||
String temp = "";
|
||||
for (int startLookingFromIndex = 0; startLookingFromIndex != -1;) {
|
||||
startLookingFromIndex = message.indexOf(heading, startLookingFromIndex);
|
||||
if (startLookingFromIndex >= 0) {
|
||||
temp = Helper.fetchXML(message.substring(startLookingFromIndex), heading, key);
|
||||
if (!temp.isEmpty()) {
|
||||
logger.trace("String was found:{}", temp);
|
||||
results.add(temp);
|
||||
++startLookingFromIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
void parseProfiles(String message) {
|
||||
mediaProfileTokens = listOfResults(message, "<trt:Profiles", "token=\"");
|
||||
if (mediaProfileIndex >= mediaProfileTokens.size()) {
|
||||
logger.warn(
|
||||
"You have set the media profile to {} when the camera reported {} profiles. Falling back to mainstream 0.",
|
||||
mediaProfileIndex, mediaProfileTokens.size());
|
||||
mediaProfileIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void processPTZLocation(String result) {
|
||||
logger.debug("Processing new PTZ location now");
|
||||
|
||||
int beginIndex = result.indexOf("x=\"");
|
||||
int endIndex = result.indexOf("\"", (beginIndex + 3));
|
||||
if (beginIndex >= 0 && endIndex >= 0) {
|
||||
currentPanCamValue = Float.parseFloat(result.substring(beginIndex + 3, endIndex));
|
||||
currentPanPercentage = (((panRangeMin - currentPanCamValue) * -1) / ((panRangeMin - panRangeMax) * -1))
|
||||
* 100;
|
||||
logger.debug("Pan is updating to:{} and the cam value is {}", Math.round(currentPanPercentage),
|
||||
currentPanCamValue);
|
||||
} else {
|
||||
logger.warn(
|
||||
"Binding could not determin the cameras current PTZ location. Not all cameras respond to GetStatus requests.");
|
||||
return;
|
||||
}
|
||||
|
||||
beginIndex = result.indexOf("y=\"");
|
||||
endIndex = result.indexOf("\"", (beginIndex + 3));
|
||||
if (beginIndex >= 0 && endIndex >= 0) {
|
||||
currentTiltCamValue = Float.parseFloat(result.substring(beginIndex + 3, endIndex));
|
||||
currentTiltPercentage = (((tiltRangeMin - currentTiltCamValue) * -1) / ((tiltRangeMin - tiltRangeMax) * -1))
|
||||
* 100;
|
||||
logger.debug("Tilt is updating to:{} and the cam value is {}", Math.round(currentTiltPercentage),
|
||||
currentTiltCamValue);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
beginIndex = result.lastIndexOf("x=\"");
|
||||
endIndex = result.indexOf("\"", (beginIndex + 3));
|
||||
if (beginIndex >= 0 && endIndex >= 0) {
|
||||
currentZoomCamValue = Float.parseFloat(result.substring(beginIndex + 3, endIndex));
|
||||
currentZoomPercentage = (((zoomMin - currentZoomCamValue) * -1) / ((zoomMin - zoomMax) * -1)) * 100;
|
||||
logger.debug("Zoom is updating to:{} and the cam value is {}", Math.round(currentZoomPercentage),
|
||||
currentZoomCamValue);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void sendPTZRequest(RequestType requestType) {
|
||||
sendOnvifRequest(requestBuilder(requestType, ptzXAddr));
|
||||
}
|
||||
|
||||
public void sendEventRequest(RequestType requestType) {
|
||||
sendOnvifRequest(requestBuilder(requestType, eventXAddr));
|
||||
}
|
||||
|
||||
public void connect(boolean useEvents) {
|
||||
if (!isConnected) {
|
||||
sendOnvifRequest(requestBuilder(RequestType.GetSystemDateAndTime, deviceXAddr));
|
||||
usingEvents = useEvents;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return isConnected;
|
||||
}
|
||||
|
||||
public void disconnect() {
|
||||
if (usingEvents && isConnected) {
|
||||
sendOnvifRequest(requestBuilder(RequestType.Unsubscribe, subscriptionXAddr));
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
isConnected = false;
|
||||
presetTokens.clear();
|
||||
mediaProfileTokens.clear();
|
||||
if (!mainEventLoopGroup.isShutdown()) {
|
||||
try {
|
||||
mainEventLoopGroup.awaitTermination(3, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
logger.info("Onvif was not shutdown correctly due to being interrupted");
|
||||
} finally {
|
||||
mainEventLoopGroup = new NioEventLoopGroup();
|
||||
bootstrap = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
|
||||
package org.openhab.binding.ipcamera.internal.onvif;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketException;
|
||||
import java.net.URL;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Enumeration;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.Helper;
|
||||
import org.openhab.binding.ipcamera.internal.IpCameraDiscoveryService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelFactory;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.DatagramChannel;
|
||||
import io.netty.channel.socket.DatagramPacket;
|
||||
import io.netty.channel.socket.InternetProtocolFamily;
|
||||
import io.netty.channel.socket.nio.NioDatagramChannel;
|
||||
import io.netty.util.CharsetUtil;
|
||||
|
||||
/**
|
||||
* The {@link OnvifDiscovery} is responsible for finding cameras that are Onvif using UDP multicast.
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class OnvifDiscovery {
|
||||
private IpCameraDiscoveryService ipCameraDiscoveryService;
|
||||
private final Logger logger = LoggerFactory.getLogger(OnvifDiscovery.class);
|
||||
public ArrayList<DatagramPacket> listOfReplys = new ArrayList<DatagramPacket>(10);
|
||||
|
||||
public OnvifDiscovery(IpCameraDiscoveryService ipCameraDiscoveryService) {
|
||||
this.ipCameraDiscoveryService = ipCameraDiscoveryService;
|
||||
}
|
||||
|
||||
public @Nullable NetworkInterface getLocalNIF() {
|
||||
try {
|
||||
for (Enumeration<NetworkInterface> enumNetworks = NetworkInterface.getNetworkInterfaces(); enumNetworks
|
||||
.hasMoreElements();) {
|
||||
NetworkInterface networkInterface = enumNetworks.nextElement();
|
||||
for (Enumeration<InetAddress> enumIpAddr = networkInterface.getInetAddresses(); enumIpAddr
|
||||
.hasMoreElements();) {
|
||||
InetAddress inetAddress = enumIpAddr.nextElement();
|
||||
if (!inetAddress.isLoopbackAddress() && inetAddress.getHostAddress().toString().length() < 18
|
||||
&& inetAddress.isSiteLocalAddress()) {
|
||||
return networkInterface;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SocketException ex) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void searchReply(String url, String xml) {
|
||||
String ipAddress = "";
|
||||
String temp = url;
|
||||
BigDecimal onvifPort = new BigDecimal(80);
|
||||
|
||||
logger.info("Camera found at xAddr:{}", url);
|
||||
int endIndex = temp.indexOf(" ");// Some xAddr have two urls with a space in between.
|
||||
if (endIndex > 0) {
|
||||
temp = temp.substring(0, endIndex);// Use only the first url from now on.
|
||||
}
|
||||
|
||||
int beginIndex = temp.indexOf(":") + 3;// add 3 to ignore the :// after http.
|
||||
int secondIndex = temp.indexOf(":", beginIndex); // find second :
|
||||
endIndex = temp.indexOf("/", beginIndex);
|
||||
if (secondIndex > beginIndex && endIndex > secondIndex) {// http://192.168.0.1:8080/onvif/device_service
|
||||
ipAddress = temp.substring(beginIndex, secondIndex);
|
||||
onvifPort = new BigDecimal(temp.substring(secondIndex + 1, endIndex));
|
||||
} else {// // http://192.168.0.1/onvif/device_service
|
||||
ipAddress = temp.substring(beginIndex, endIndex);
|
||||
}
|
||||
String brand = checkForBrand(xml);
|
||||
if (brand.equals("onvif")) {
|
||||
try {
|
||||
brand = getBrandFromLoginPage(ipAddress);
|
||||
} catch (IOException e) {
|
||||
brand = "onvif";
|
||||
}
|
||||
}
|
||||
ipCameraDiscoveryService.newCameraFound(brand, ipAddress, onvifPort.intValue());
|
||||
}
|
||||
|
||||
void processCameraReplys() {
|
||||
for (DatagramPacket packet : listOfReplys) {
|
||||
logger.trace("Device replied to discovery with:{}", packet.toString());
|
||||
String xml = packet.content().toString(CharsetUtil.UTF_8);
|
||||
String xAddr = Helper.fetchXML(xml, "", "<d:XAddrs>");
|
||||
if (!xAddr.equals("")) {
|
||||
searchReply(xAddr, xml);
|
||||
} else if (xml.contains("onvif")) {
|
||||
logger.info("Possible ONVIF camera found at:{}", packet.sender().getHostString());
|
||||
ipCameraDiscoveryService.newCameraFound("onvif", packet.sender().getHostString(), 80);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String checkForBrand(String response) {
|
||||
if (response.toLowerCase().contains("amcrest")) {
|
||||
return "dahua";
|
||||
} else if (response.toLowerCase().contains("dahua")) {
|
||||
return "dahua";
|
||||
} else if (response.toLowerCase().contains("foscam")) {
|
||||
return "foscam";
|
||||
} else if (response.toLowerCase().contains("hikvision")) {
|
||||
return "hikvision";
|
||||
} else if (response.toLowerCase().contains("instar")) {
|
||||
return "instar";
|
||||
} else if (response.toLowerCase().contains("doorbird")) {
|
||||
return "doorbird";
|
||||
} else if (response.toLowerCase().contains("ipc-")) {
|
||||
return "dahua";
|
||||
} else if (response.toLowerCase().contains("dh-sd")) {
|
||||
return "dahua";
|
||||
}
|
||||
return "onvif";
|
||||
}
|
||||
|
||||
public String getBrandFromLoginPage(String hostname) throws IOException {
|
||||
URL url = new URL("http://" + hostname);
|
||||
String brand = "onvif";
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setConnectTimeout(1000);
|
||||
connection.setReadTimeout(2000);
|
||||
connection.setInstanceFollowRedirects(true);
|
||||
connection.setRequestMethod("GET");
|
||||
try {
|
||||
connection.connect();
|
||||
BufferedReader reply = new BufferedReader(new InputStreamReader(connection.getInputStream()));
|
||||
String response = "";
|
||||
String temp;
|
||||
while ((temp = reply.readLine()) != null) {
|
||||
response += temp;
|
||||
}
|
||||
reply.close();
|
||||
logger.trace("Cameras Login page is:{}", response);
|
||||
brand = checkForBrand(response);
|
||||
} catch (MalformedURLException e) {
|
||||
} finally {
|
||||
connection.disconnect();
|
||||
}
|
||||
return brand;
|
||||
}
|
||||
|
||||
public void discoverCameras(int port) throws UnknownHostException, InterruptedException {
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
String xml = "";
|
||||
|
||||
if (port == 3702) {
|
||||
xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><e:Envelope xmlns:e=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:w=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\" xmlns:d=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\" xmlns:dn=\"http://www.onvif.org/ver10/network/wsdl\"><e:Header><w:MessageID>uuid:"
|
||||
+ uuid
|
||||
+ "</w:MessageID><w:To e:mustUnderstand=\"true\">urn:schemas-xmlsoap-org:ws:2005:04:discovery</w:To><w:Action a:mustUnderstand=\"true\">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</w:Action></e:Header><e:Body><d:Probe><d:Types xmlns:dp0=\"http://www.onvif.org/ver10/network/wsdl\">dp0:NetworkVideoTransmitter</d:Types></d:Probe></e:Body></e:Envelope>";
|
||||
}
|
||||
ByteBuf discoveryProbeMessage = Unpooled.copiedBuffer(xml, 0, xml.length(), StandardCharsets.UTF_8);
|
||||
InetSocketAddress localNetworkAddress = new InetSocketAddress(0);// Listen for replies on all connections.
|
||||
InetSocketAddress multiCastAddress = new InetSocketAddress(InetAddress.getByName("239.255.255.250"), port);
|
||||
DatagramPacket datagramPacket = new DatagramPacket(discoveryProbeMessage, multiCastAddress,
|
||||
localNetworkAddress);
|
||||
NetworkInterface networkInterface = getLocalNIF();
|
||||
DatagramChannel datagramChannel;
|
||||
|
||||
Bootstrap bootstrap = new Bootstrap().group(new NioEventLoopGroup())
|
||||
.channelFactory(new ChannelFactory<NioDatagramChannel>() {
|
||||
@Override
|
||||
public NioDatagramChannel newChannel() {
|
||||
return new NioDatagramChannel(InternetProtocolFamily.IPv4);
|
||||
}
|
||||
}).handler(new SimpleChannelInboundHandler<DatagramPacket>() {
|
||||
@Override
|
||||
protected void channelRead0(@Nullable ChannelHandlerContext ctx, DatagramPacket msg)
|
||||
throws Exception {
|
||||
msg.retain(1);
|
||||
listOfReplys.add(msg);
|
||||
}
|
||||
}).option(ChannelOption.SO_BROADCAST, true).option(ChannelOption.SO_REUSEADDR, true)
|
||||
.option(ChannelOption.IP_MULTICAST_LOOP_DISABLED, false).option(ChannelOption.SO_RCVBUF, 2048)
|
||||
.option(ChannelOption.IP_MULTICAST_TTL, 255).option(ChannelOption.IP_MULTICAST_IF, networkInterface);
|
||||
|
||||
datagramChannel = (DatagramChannel) bootstrap.bind(localNetworkAddress).sync().channel();
|
||||
datagramChannel.joinGroup(multiCastAddress, networkInterface).sync();
|
||||
ChannelFuture chFuture;
|
||||
if (port == 1900) {
|
||||
String ssdp = "M-SEARCH * HTTP/1.1\n" + "HOST: 239.255.255.250:1900\n" + "MAN: \"ssdp:discover\"\n"
|
||||
+ "MX: 1\n" + "ST: urn:dial-multiscreen-org:service:dial:1\n"
|
||||
+ "USER-AGENT: Microsoft Edge/83.0.478.61 Windows\n" + "\n" + "";
|
||||
ByteBuf ssdpProbeMessage = Unpooled.copiedBuffer(ssdp, 0, ssdp.length(), StandardCharsets.UTF_8);
|
||||
datagramPacket = new DatagramPacket(ssdpProbeMessage, multiCastAddress, localNetworkAddress);
|
||||
chFuture = datagramChannel.writeAndFlush(datagramPacket);
|
||||
} else {
|
||||
chFuture = datagramChannel.writeAndFlush(datagramPacket);
|
||||
}
|
||||
chFuture.awaitUninterruptibly(2000);
|
||||
chFuture = datagramChannel.closeFuture();
|
||||
TimeUnit.SECONDS.sleep(5);
|
||||
datagramChannel.close();
|
||||
chFuture.awaitUninterruptibly(6000);
|
||||
processCameraReplys();
|
||||
bootstrap.config().group().shutdownGracefully();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.ipcamera.internal.rtsp;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.http.LastHttpContent;
|
||||
|
||||
/**
|
||||
* The {@link NettyRtspHandler} is used to decode RTSP traffic into message Strings.
|
||||
*
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NettyRtspHandler extends ChannelDuplexHandler {
|
||||
RtspConnection rtspConnection;
|
||||
|
||||
NettyRtspHandler(RtspConnection rtspConnection) {
|
||||
this.rtspConnection = rtspConnection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(@Nullable ChannelHandlerContext ctx, @Nullable Object msg) throws Exception {
|
||||
if (msg == null || ctx == null) {
|
||||
return;
|
||||
}
|
||||
if (!(msg instanceof LastHttpContent)) {
|
||||
rtspConnection.processMessage(msg);
|
||||
} else {
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelReadComplete(@Nullable ChannelHandlerContext ctx) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerAdded(@Nullable ChannelHandlerContext ctx) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerRemoved(@Nullable ChannelHandlerContext ctx) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(@Nullable ChannelHandlerContext ctx, @Nullable Throwable cause) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void userEventTriggered(@Nullable ChannelHandlerContext ctx, @Nullable Object evt) throws Exception {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.ipcamera.internal.rtsp;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.ipcamera.internal.handler.IpCameraHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||
import io.netty.handler.codec.http.DefaultHttpRequest;
|
||||
import io.netty.handler.codec.http.HttpRequest;
|
||||
import io.netty.handler.codec.rtsp.RtspDecoder;
|
||||
import io.netty.handler.codec.rtsp.RtspEncoder;
|
||||
import io.netty.handler.codec.rtsp.RtspHeaderNames;
|
||||
import io.netty.handler.codec.rtsp.RtspMethods;
|
||||
import io.netty.handler.codec.rtsp.RtspVersions;
|
||||
import io.netty.handler.timeout.IdleStateHandler;
|
||||
|
||||
/**
|
||||
* The {@link RtspConnection} is a WIP and not currently used, but will talk directly to RTSP and collect information
|
||||
* about the camera and streams.
|
||||
*
|
||||
*
|
||||
* @author Matthew Skinner - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RtspConnection {
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
private @Nullable Bootstrap rtspBootstrap;
|
||||
private EventLoopGroup mainEventLoopGroup = new NioEventLoopGroup();
|
||||
private IpCameraHandler ipCameraHandler;
|
||||
String username, password;
|
||||
|
||||
public RtspConnection(IpCameraHandler ipCameraHandler, String username, String password) {
|
||||
this.ipCameraHandler = ipCameraHandler;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public void connect() {
|
||||
sendRtspRequest(getRTSPoptions());
|
||||
}
|
||||
|
||||
public void processMessage(Object msg) {
|
||||
logger.info("reply from RTSP is {}", msg);
|
||||
if (msg.toString().contains("DESCRIBE")) {// getRTSPoptions
|
||||
// Public: OPTIONS, DESCRIBE, ANNOUNCE, SETUP, PLAY, RECORD, PAUSE, TEARDOWN, SET_PARAMETER, GET_PARAMETER
|
||||
sendRtspRequest(getRTSPdescribe());
|
||||
} else if (msg.toString().contains("CSeq: 2")) {// getRTSPdescribe
|
||||
// returns this:
|
||||
// RTSP/1.0 200 OK
|
||||
// CSeq: 2
|
||||
// x-Accept-Dynamic-Rate: 1
|
||||
// Content-Base:
|
||||
// rtsp://192.168.xx.xx:554/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif/
|
||||
// Cache-Control: must-revalidate
|
||||
// Content-Length: 582
|
||||
// Content-Type: application/sdp
|
||||
sendRtspRequest(getRTSPsetup());
|
||||
} else if (msg.toString().contains("CSeq: 3")) {
|
||||
sendRtspRequest(getRTSPplay());
|
||||
}
|
||||
}
|
||||
|
||||
HttpRequest getRTSPoptions() {
|
||||
HttpRequest request = new DefaultHttpRequest(RtspVersions.RTSP_1_0, RtspMethods.OPTIONS,
|
||||
ipCameraHandler.rtspUri);
|
||||
request.headers().add(RtspHeaderNames.CSEQ, "1");
|
||||
return request;
|
||||
}
|
||||
|
||||
HttpRequest getRTSPdescribe() {
|
||||
HttpRequest request = new DefaultHttpRequest(RtspVersions.RTSP_1_0, RtspMethods.DESCRIBE,
|
||||
ipCameraHandler.rtspUri);
|
||||
request.headers().add(RtspHeaderNames.CSEQ, "2");
|
||||
return request;
|
||||
}
|
||||
|
||||
HttpRequest getRTSPsetup() {
|
||||
HttpRequest request = new DefaultHttpRequest(RtspVersions.RTSP_1_0, RtspMethods.SETUP, ipCameraHandler.rtspUri);
|
||||
request.headers().add(RtspHeaderNames.CSEQ, "3");
|
||||
request.headers().add(RtspHeaderNames.TRANSPORT, "RTP/AVP;unicast;client_port=5000-5001");
|
||||
return request;
|
||||
}
|
||||
|
||||
HttpRequest getRTSPplay() {
|
||||
HttpRequest request = new DefaultHttpRequest(RtspVersions.RTSP_1_0, RtspMethods.PLAY, ipCameraHandler.rtspUri);
|
||||
request.headers().add(RtspHeaderNames.CSEQ, "4");
|
||||
// need session to match response from getRTSPsetup()
|
||||
request.headers().add(RtspHeaderNames.SESSION, "12345678");
|
||||
return request;
|
||||
}
|
||||
|
||||
private RtspConnection getHandle() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
public void sendRtspRequest(HttpRequest request) {
|
||||
if (rtspBootstrap == null) {
|
||||
rtspBootstrap = new Bootstrap();
|
||||
rtspBootstrap.group(mainEventLoopGroup);
|
||||
rtspBootstrap.channel(NioSocketChannel.class);
|
||||
rtspBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
|
||||
rtspBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4500);
|
||||
rtspBootstrap.option(ChannelOption.SO_SNDBUF, 1024 * 8);
|
||||
rtspBootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
|
||||
rtspBootstrap.option(ChannelOption.TCP_NODELAY, true);
|
||||
rtspBootstrap.handler(new ChannelInitializer<SocketChannel>() {
|
||||
|
||||
@Override
|
||||
public void initChannel(SocketChannel socketChannel) throws Exception {
|
||||
socketChannel.pipeline().addLast(new IdleStateHandler(18, 0, 0));
|
||||
socketChannel.pipeline().addLast(new RtspDecoder());
|
||||
socketChannel.pipeline().addLast(new RtspEncoder());
|
||||
// Need to update the authhandler to work for multiple use cases, before this works.
|
||||
// socketChannel.pipeline().addLast(new MyNettyAuthHandler(username, password, ipCameraHandler));
|
||||
socketChannel.pipeline().addLast(new NettyRtspHandler(getHandle()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
rtspBootstrap.connect(new InetSocketAddress(ipCameraHandler.cameraConfig.getIp(), 554))
|
||||
.addListener(new ChannelFutureListener() {
|
||||
|
||||
@Override
|
||||
public void operationComplete(@Nullable ChannelFuture future) {
|
||||
if (future == null) {
|
||||
return;
|
||||
}
|
||||
if (future.isDone() && future.isSuccess()) {
|
||||
Channel ch = future.channel();
|
||||
ch.writeAndFlush(request);
|
||||
} else { // an error occured
|
||||
logger.debug("Could not reach cameras rtsp on port 554.");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="ipcamera" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>IpCamera Binding</name>
|
||||
<description>This binding helps you to use IP Cameras in Openhab 2.</description>
|
||||
<author>Matthew Skinner</author>
|
||||
</binding:binding>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user