added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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.");
}
}
}

View File

@@ -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";
}

View File

@@ -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.");
}
}
}

View File

@@ -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;
}
}

View File

@@ -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) {
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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();
}
}

View File

@@ -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 {
}
}

View File

@@ -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.");
}
}
});
}
}

View File

@@ -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>