Skip to content

Commit

Permalink
[ipcamera] Add white LED controls for Dahua and also Email and Push f…
Browse files Browse the repository at this point in the history
…or Reolink with v20 command support (openhab#16144)

* New reolink channels
* extra channel for Dahua.
* Reolink NPE fix
* Fix LED modes and auto.
* Handle NVR channels for new channels
* add nvr channels to Dahua.

Signed-off-by: Matthew Skinner <matt@pcmus.com>
Signed-off-by: Andras Uhrin <andras.uhrin@gmail.com>
  • Loading branch information
Skinah authored and andrasU committed Jan 27, 2024
1 parent 85b17e1 commit 438b6dc
Show file tree
Hide file tree
Showing 10 changed files with 669 additions and 266 deletions.
128 changes: 66 additions & 62 deletions bundles/org.openhab.binding.ipcamera/README.md

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,10 @@ public enum FFmpegFormat {
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_WHITE_LED = "autoWhiteLED";
public static final String CHANNEL_AUTO_LED = "autoLED";
public static final String CHANNEL_ENABLE_LED = "enableLED";
public static final String CHANNEL_WHITE_LED = "whiteLED";
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";
Expand All @@ -143,5 +145,7 @@ public enum FFmpegFormat {
public static final String CHANNEL_HUMAN_ALARM = "humanAlarm";
public static final String CHANNEL_ANIMAL_ALARM = "animalAlarm";
public static final String CHANNEL_ENABLE_FTP = "enableFTP";
public static final String CHANNEL_ENABLE_EMAIL = "enableEmail";
public static final String CHANNEL_ENABLE_PUSH = "enablePush";
public static final String CHANNEL_ENABLE_RECORDINGS = "enableRecordings";
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@

import org.eclipse.jdt.annotation.NonNullByDefault;

import com.google.gson.annotations.SerializedName;

/**
* The {@link ReolinkState} class holds the state and GSON parsed replies for a single Reolink Camera.
* The {@link ReolinkState} DTO holds the state and GSON parsed replies from a Reolink Camera.
*
* @author Matthew Skinner - Initial contribution
*/
Expand All @@ -24,19 +26,59 @@ public class ReolinkState {
public class GetAiStateResponse {
public class Value {
public class Alarm {
public int alarm_state = 0;
public int support = 0;
@SerializedName(value = "alarmState", alternate = { "alarm_state" }) // alarm_state is used in json
public int alarmState = 0;
}

public int channel = 0;
public Alarm dog_cat = new Alarm();
@SerializedName(value = "dogCat", alternate = { "dog_cat" }) // dog_cat is used in json
public Alarm dogCat = new Alarm();
public Alarm face = new Alarm();
public Alarm people = new Alarm();
public Alarm vehicle = new Alarm();
}

public String cmd = "";
public int code = 0;
public class Error {
public String detail = "";
}

public Value value = new Value();
public Error error = new Error();
}

public class GetAbilityResponse {
public class Value {
public class Ability {
public class AbilityKey {
public int permit = 0;
public int ver = 0;
}

public class AbilityChn {
public AbilityKey supportAiFace = new AbilityKey();
public AbilityKey supportAiPeople = new AbilityKey();
public AbilityKey supportAiVehicle = new AbilityKey();
public AbilityKey supportAiDogCat = new AbilityKey();
}

public AbilityChn[] abilityChn = new AbilityChn[1];
public AbilityKey push = new AbilityKey();
public AbilityKey scheduleVersion = new AbilityKey();
public AbilityKey supportAudioAlarm = new AbilityKey();
public AbilityKey supportAudioAlarmEnable = new AbilityKey();
public AbilityKey supportEmailEnable = new AbilityKey();
public AbilityKey supportFtpEnable = new AbilityKey();
public AbilityKey supportRecordEnable = new AbilityKey();
}

@SerializedName(value = "ability", alternate = { "Ability" }) // uses uppercase A
public Ability ability = new Ability();
}

public class Error {
public String detail = "";
}

public Value value = new Value();
public Error error = new Error();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;

import org.eclipse.jdt.annotation.NonNullByDefault;
Expand Down Expand Up @@ -173,6 +174,7 @@ public class IpCameraHandler extends BaseThingHandler {
// basicAuth MUST remain private as it holds the cameraConfig.getPassword()
private String basicAuth = "";
public String reolinkAuth = "&token=null";
public int reolinkScheduleVersion = 0;
public boolean useBasicAuth = false;
public boolean useDigestAuth = false;
public boolean newInstarApi = false;
Expand All @@ -183,7 +185,7 @@ public class IpCameraHandler extends BaseThingHandler {
public String rtspUri = "";
public boolean audioAlarmUpdateSnapshot = false;
private boolean motionAlarmUpdateSnapshot = false;
private boolean isOnline = false; // Used so only 1 error is logged when a network issue occurs.
private AtomicBoolean isOnline = new AtomicBoolean(); // Used so only 1 error is logged when a network issue occurs.
private boolean firstAudioAlarm = false;
private boolean firstMotionAlarm = false;
public BigDecimal motionThreshold = BigDecimal.ZERO;
Expand Down Expand Up @@ -436,7 +438,7 @@ public boolean setBasicAuth(boolean useBasic) {
return false;
}

private String getCorrectUrlFormat(String longUrl) {
public String getCorrectUrlFormat(String longUrl) {
String temp = longUrl;
URL url;

Expand Down Expand Up @@ -516,13 +518,12 @@ private void checkCameraConnection() {
Ffmpeg localSnapshot = ffmpegSnapshot;
if (localSnapshot != null && !localSnapshot.isAlive()) {
cameraCommunicationError("FFmpeg Snapshots Stopped: Check that your camera can be reached.");
return;
}
return; // ffmpeg snapshot stream is still alive
}

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

Expand All @@ -536,8 +537,8 @@ private void checkCameraConnection() {
return;
}
}
cameraCommunicationError(
"Connection Timeout: Check your IP and PORT are correct and the camera can be reached.");
cameraCommunicationError("Connection Timeout: Check your IP:" + cameraConfig.getIp() + " and PORT:"
+ cameraConfig.getPort() + " are correct and the camera can be reached.");
}

// Always use this as sendHttpGET(GET/POST/PUT/DELETE, "/foo/bar",null)//
Expand All @@ -546,7 +547,7 @@ private void checkCameraConnection() {
public void sendHttpRequest(String httpMethod, String httpRequestURLFull, @Nullable String digestString) {
int port = getPortFromShortenedUrl(httpRequestURLFull);
String httpRequestURL = getTinyUrl(httpRequestURLFull);

logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port, httpRequestURL);
if (mainBootstrap == null) {
mainBootstrap = new Bootstrap();
mainBootstrap.group(mainEventLoopGroup);
Expand Down Expand Up @@ -637,12 +638,9 @@ public void operationComplete(@Nullable ChannelFuture future) {
if (future.isDone() && future.isSuccess()) {
Channel ch = future.channel();
openChannels.add(ch);
if (!isOnline) {
if (cameraConnectionJob != null && !isOnline.get()) {
bringCameraOnline();
}
logger.trace("Sending camera: {}: http://{}:{}{}", httpMethod, cameraConfig.getIp(), port,
httpRequestURL);

openChannel(ch, httpRequestURL);
CommonCameraHandler commonHandler = (CommonCameraHandler) ch.pipeline().get(COMMON_HANDLER);
commonHandler.setURL(httpRequestURLFull);
Expand Down Expand Up @@ -1350,7 +1348,7 @@ public void setChannelState(String channelToUpdate, State valueOf) {
}

private void bringCameraOnline() {
isOnline = true;
isOnline.set(true);
updateStatus(ThingStatus.ONLINE);
groupTracker.listOfOnlineCameraHandlers.add(this);
groupTracker.listOfOnlineCameraUID.add(getThing().getUID().getId());
Expand Down Expand Up @@ -1405,6 +1403,12 @@ void snapshotIsFfmpeg() {
}
}

/**
* The {@link pollingCameraConnection} This polls to see if the camera is reachable only until the camera
* successfully connects.
*
*/

void pollingCameraConnection() {
keepMjpegRunning();
if (thing.getThingTypeUID().getId().equals(GENERIC_THING)
Expand All @@ -1421,12 +1425,6 @@ void pollingCameraConnection() {
return;
}
if (cameraConfig.getOnvifPort() > 0 && !onvifCamera.isConnected()) {
if (onvifCamera.isConnectError()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Camera is not reachable");
} else if (onvifCamera.isRefusedError()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Camera refused connection on ONVIF ports.");
}
logger.debug("About to connect to the IP Camera using the ONVIF PORT at IP: {}:{}", cameraConfig.getIp(),
cameraConfig.getOnvifPort());
onvifCamera.connect(supportsOnvifEvents());
Expand Down Expand Up @@ -1454,7 +1452,7 @@ public void cameraConfigError(String reason) {
public void cameraCommunicationError(String reason) {
// will try to reconnect again as camera may be rebooting.
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
if (isOnline) { // if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
if (isOnline.get()) { // if already offline dont try reconnecting in 6 seconds, we want 30sec wait.
resetAndRetryConnecting();
}
}
Expand Down Expand Up @@ -1489,7 +1487,7 @@ private void updateSnapshot() {
}

public byte[] getSnapshot() {
if (!isOnline) {
if (!isOnline.get()) {
// Single gray pixel JPG to keep streams open when the camera goes offline so they dont stop.
return new byte[] { (byte) 0xff, (byte) 0xd8, (byte) 0xff, (byte) 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
0x00, 0x01, 0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, (byte) 0xff, (byte) 0xdb, 0x00, 0x43,
Expand Down Expand Up @@ -1596,13 +1594,8 @@ void pollCameraRunnable() {
+ cameraConfig.getUser() + "&password=" + cameraConfig.getPassword());
sendHttpGET("/api.cgi?cmd=GetMdState&channel=" + cameraConfig.getNvrChannel() + "&user="
+ cameraConfig.getUser() + "&password=" + cameraConfig.getPassword());
} else {
if (!snapshotPolling) {
checkCameraConnection();
}
if (!onvifCamera.isConnected()) {
onvifCamera.connect(true);
}
} else if (!snapshotPolling) {
checkCameraConnection();
}
break;
case DAHUA_THING:
Expand Down Expand Up @@ -1736,6 +1729,9 @@ public void initialize() {
TimeUnit.MINUTES);
} else {
reolinkAuth = "&user=" + cameraConfig.getUser() + "&password=" + cameraConfig.getPassword();
// The reply to api.cgi?cmd=Login also sends this only with a token
sendHttpPOST("/api.cgi?cmd=GetAbility" + reolinkAuth,
"[{ \"cmd\":\"GetAbility\", \"param\":{ \"User\":{ \"userName\":\"admin\" }}}]");
}
if (snapshotUri.isEmpty()) {
if (cameraConfig.getNvrChannel() < 1) {
Expand Down Expand Up @@ -1766,26 +1762,15 @@ public void initialize() {
}

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

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

private boolean supportsOnvifEvents() {
Expand Down Expand Up @@ -1817,7 +1802,7 @@ private void resetAndRetryConnecting() {
}

private void offline() {
isOnline = false;
isOnline.set(false);
snapshotPolling = false;
Future<?> localFuture = pollCameraJob;
if (localFuture != null) {
Expand Down

0 comments on commit 438b6dc

Please sign in to comment.