Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ipcamera] Add white LED controls for Dahua and also Email and Push for Reolink with v20 command support #16144

Merged
merged 33 commits into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
72a6199
New reolink channels
Skinah Dec 27, 2023
f2bec60
extra channel for Dahua.
Skinah Dec 27, 2023
4087dc5
Reolink NPE fix
Skinah Dec 27, 2023
6949252
Fix LED modes and auto.
Skinah Dec 27, 2023
c89ba9d
spotless
Skinah Dec 27, 2023
9ffad0d
update translations
Skinah Dec 27, 2023
7ec3d6c
Handle NVR channels for new channels
Skinah Dec 27, 2023
e0d2cdb
add nvr channels to Dahua.
Skinah Dec 27, 2023
da5e719
improvements
Skinah Dec 29, 2023
f6ba58e
Fix token use.
Skinah Dec 29, 2023
43506ef
Add V2 commands
Skinah Jan 5, 2024
05bcb67
Check users rights.
Skinah Jan 5, 2024
cbc0e61
Fix two bugs.
Skinah Jan 6, 2024
1a8782d
cleanup
Skinah Jan 6, 2024
d1f3b3f
Use more gson
Skinah Jan 7, 2024
fb89a40
Fix onvif connection bug
Skinah Jan 7, 2024
8e01f0d
Remove motion commands from white light.
Skinah Jan 8, 2024
efa4087
fix reolink stateDTO
Skinah Jan 9, 2024
6a81582
remove logging.
Skinah Jan 10, 2024
c9cb5d9
Reduce logging
Skinah Jan 15, 2024
2180b59
update readme.
Skinah Jan 25, 2024
e86a9a6
Update bundles/org.openhab.binding.ipcamera/src/main/resources/OH-INF…
Skinah Jan 25, 2024
4a53fa4
Update bundles/org.openhab.binding.ipcamera/src/main/resources/OH-INF…
Skinah Jan 25, 2024
f5fc65a
Update bundles/org.openhab.binding.ipcamera/src/main/resources/OH-INF…
Skinah Jan 25, 2024
ce9a6f8
Update bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab…
Skinah Jan 25, 2024
6b5416a
translations and spotless.
Skinah Jan 25, 2024
55bf708
Fix ONVIF reports a snapshot url at a port other than the config port.
Skinah Jan 26, 2024
65d181e
Improve logs
Skinah Jan 26, 2024
c26bc4b
better logs.
Skinah Jan 26, 2024
d102b3e
onvif fixes
Skinah Jan 26, 2024
1a77987
Fix comment and use admin for checking abilities.
Skinah Jan 26, 2024
20c0e25
Fix member name check warnings in DTO class
Skinah Jan 26, 2024
3532db1
change auto white led to mean motion turns on and off.
Skinah Jan 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -118,8 +118,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 @@ -144,5 +146,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