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

[netatmo] Support for switching video surveillance on/off (#7938) #7968

Merged
merged 11 commits into from
Jun 28, 2020
11 changes: 6 additions & 5 deletions bundles/org.openhab.binding.netatmo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -502,14 +502,17 @@ All these channels are read only.

### Welcome and Presence Camera

Warning: The URL of the live snapshot is a fixed URL so the value of the channel cameraLivePictureUrl / welcomeCameraLivePictureUrl will never be updated once first set by the binding.
Warnings:
- The URL of the live snapshot is a fixed URL so the value of the channel cameraLivePictureUrl / welcomeCameraLivePictureUrl will never be updated once first set by the binding.
So to get a refreshed picture, you need to use the refresh parameter in your sitemap image element.
- Some features like the video surveillance are accessed via the local network, so it may be helpful to set a static IP address
for the camera within your local network.

**Supported channels for the Welcome Camera thing:**

| Channel ID | Item Type | Read/Write | Description |
|-----------------------------|-----------|------------|--------------------------------------------------------------|
| welcomeCameraStatus | Switch | Read-only | State of the camera |
| welcomeCameraStatus | Switch | Read-write | State of the camera (video surveillance on/off) |
| welcomeCameraSdStatus | Switch | Read-only | State of the SD card |
| welcomeCameraAlimStatus | Switch | Read-only | State of the power connector |
| welcomeCameraIsLocal | Switch | Read-only | indicates whether the camera is on the same network than the openHAB Netatmo Binding |
Expand All @@ -520,15 +523,13 @@ So to get a refreshed picture, you need to use the refresh parameter in your sit
**Supported channels for the Presence Camera thing:**

Warnings:
- Some features like the floodlight are accessed via the local network, so it may be helpful to set a static IP address
for the Presence camera within your local network.
- The floodlight auto-mode (cameraFloodlightAutoMode) isn't updated it is changed by another application. Therefore the
binding handles its own state of the auto-mode. This has the advantage that the user can define its own floodlight
switch off behaviour.

| Channel ID | Item Type | Read/Write | Description |
|-----------------------------|-----------|------------|--------------------------------------------------------------|
| cameraStatus | Switch | Read-only | State of the camera |
| cameraStatus | Switch | Read-write | State of the camera (video surveillance on/off) |
| cameraSdStatus | Switch | Read-only | State of the SD card |
| cameraAlimStatus | Switch | Read-only | State of the power connector |
| cameraIsLocal | Switch | Read-only | indicates whether the camera is on the same network than the openHAB Netatmo Binding |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.netatmo.internal.presence;
package org.openhab.binding.netatmo.internal.camera;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,26 @@
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.smarthome.core.i18n.TimeZoneProvider;
import org.eclipse.smarthome.core.library.types.StringType;
import org.eclipse.smarthome.core.library.types.OnOffType;
import org.eclipse.smarthome.core.thing.ChannelUID;
import org.eclipse.smarthome.core.thing.Thing;
import org.eclipse.smarthome.core.types.Command;
import org.eclipse.smarthome.core.types.State;
import org.eclipse.smarthome.core.types.UnDefType;
import org.eclipse.smarthome.io.net.http.HttpUtil;
import org.json.JSONException;
import org.json.JSONObject;
import org.openhab.binding.netatmo.internal.ChannelTypeUtils;
import org.openhab.binding.netatmo.internal.handler.NetatmoModuleHandler;

import io.swagger.client.model.NAWelcomeCamera;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.Optional;

/**
* {@link CameraHandler} is the class used to handle Camera Data
Expand All @@ -33,20 +44,42 @@
* NAWelcomeCameraHandler)
*
*/
@NonNullByDefault
public abstract class CameraHandler extends NetatmoModuleHandler<NAWelcomeCamera> {

private static final String PING_URL_PATH = "/command/ping";
private static final String STATUS_CHANGE_URL_PATH = "/command/changestatus";
private static final String LIVE_PICTURE = "/live/snapshot_720.jpg";

protected CameraHandler(@NonNull Thing thing, final TimeZoneProvider timeZoneProvider) {
private final Logger logger = LoggerFactory.getLogger(CameraHandler.class);

private Optional<CameraAddress> cameraAddress = Optional.empty();

protected CameraHandler(Thing thing, final TimeZoneProvider timeZoneProvider) {
super(thing, timeZoneProvider);
}

@Override
public void handleCommand(ChannelUID channelUID, Command command) {
String channelId = channelUID.getId();
switch (channelId) {
case CHANNEL_CAMERA_STATUS:
case CHANNEL_WELCOME_CAMERA_STATUS:
if(command == OnOffType.ON) {
switchVideoSurveillance(true);
} else if(command == OnOffType.OFF) {
switchVideoSurveillance(false);
}
break;
}
super.handleCommand(channelUID, command);
}

@Override
protected void updateProperties(NAWelcomeCamera moduleData) {
updateProperties(null, moduleData.getType());
}

@SuppressWarnings("null")
@Override
protected State getNAThingProperty(@NonNull String channelId) {
switch (channelId) {
Expand All @@ -69,93 +102,136 @@ protected State getNAThingProperty(@NonNull String channelId) {
}

protected State getStatusState() {
return module != null ? toOnOffType(module.getStatus()) : UnDefType.UNDEF;
return getModule().map(m -> toOnOffType(m.getStatus())).orElse(UnDefType.UNDEF);
}

protected State getSdStatusState() {
return module != null ? toOnOffType(module.getSdStatus()) : UnDefType.UNDEF;
return getModule().map(m -> toOnOffType(m.getSdStatus())).orElse(UnDefType.UNDEF);
}

protected State getAlimStatusState() {
return module != null ? toOnOffType(module.getAlimStatus()) : UnDefType.UNDEF;
return getModule().map(m -> toOnOffType(m.getAlimStatus())).orElse(UnDefType.UNDEF);
}

protected State getIsLocalState() {
return module != null ? toOnOffType(module.getIsLocal()) : UnDefType.UNDEF;
return getModule().map(m -> toOnOffType(m.getIsLocal())).orElse(UnDefType.UNDEF);
}

protected State getLivePictureURLState() {
String livePictureURL = getLivePictureURL();
return livePictureURL == null ? UnDefType.UNDEF : toStringType(livePictureURL);
return getLivePictureURL().map(ChannelTypeUtils::toStringType).orElse(UnDefType.UNDEF);
}

protected State getLivePictureState() {
String livePictureURL = getLivePictureURL();
return livePictureURL == null ? UnDefType.UNDEF : HttpUtil.downloadImage(livePictureURL);
Optional<String> livePictureURL = getLivePictureURL();
return livePictureURL.isPresent() ? HttpUtil.downloadImage(livePictureURL.get()) : UnDefType.UNDEF;
}

protected State getLiveStreamState() {
String liveStreamURL = getLiveStreamURL();
return liveStreamURL == null ? UnDefType.UNDEF : new StringType(liveStreamURL);
return getLiveStreamURL().map(ChannelTypeUtils::toStringType).orElse(UnDefType.UNDEF);
}

/**
* Get the url for the live snapshot
*
* @return Url of the live snapshot
*/
private String getLivePictureURL() {
String result = getVpnUrl();
if (result != null) {
result += LIVE_PICTURE;
}
return result;
private Optional<String> getLivePictureURL() {
return getVpnUrl().map(u -> u += LIVE_PICTURE);
}

/**
* Get the url for the live stream depending wether local or not
*
* @return Url of the live stream
*/
private String getLiveStreamURL() {
String result = getVpnUrl();
if (result == null) {
return null;
private Optional<String> getLiveStreamURL() {
Optional<String> result = getVpnUrl();
if (!result.isPresent()) {
return Optional.empty();
}

StringBuilder resultStringBuilder = new StringBuilder(result);
StringBuilder resultStringBuilder = new StringBuilder(result.get());
resultStringBuilder.append("/live/index");
if (isLocal()) {
resultStringBuilder.append("_local");
}
resultStringBuilder.append(".m3u8");
return resultStringBuilder.toString();
return Optional.of(resultStringBuilder.toString());
}

@SuppressWarnings("null")
protected String getVpnUrl() {
return (module == null) ? null : module.getVpnUrl();
private Optional<String> getVpnUrl() {
return getModule().map(NAWelcomeCamera::getVpnUrl);
}

public String getStreamURL(String videoId) {
String result = getVpnUrl();
if (result == null) {
return null;
public Optional<String> getStreamURL(String videoId) {
Optional<String> result = getVpnUrl();
if (!result.isPresent()) {
return Optional.empty();
}

StringBuilder resultStringBuilder = new StringBuilder(result);
StringBuilder resultStringBuilder = new StringBuilder(result.get());
resultStringBuilder.append("/vod/");
resultStringBuilder.append(videoId);
resultStringBuilder.append("/index");
if (isLocal()) {
resultStringBuilder.append("_local");
}
resultStringBuilder.append(".m3u8");
return resultStringBuilder.toString();
return Optional.of(resultStringBuilder.toString());
}

@SuppressWarnings("null")
private boolean isLocal() {
return (module == null || module.getIsLocal() == null) ? false : module.getIsLocal();
return getModule().map(NAWelcomeCamera::getIsLocal).orElse(false);
}

private void switchVideoSurveillance(boolean isOn) {
Optional<String> localCameraURL = getLocalCameraURL();
if (localCameraURL.isPresent()) {
String url = localCameraURL.get() + STATUS_CHANGE_URL_PATH + "?status=";
if(isOn) {
url += "on";
} else {
url += "off";
}
executeGETRequest(url);

invalidateParentCacheAndRefresh();
}
}

protected Optional<String> getLocalCameraURL() {
Optional<String> vpnURLOptional = getVpnUrl();
if (vpnURLOptional.isPresent()) {
final String vpnURL = vpnURLOptional.get();

//The local address is (re-)requested when it wasn't already determined or when the vpn address was changed.
if (!cameraAddress.isPresent() || cameraAddress.get().isVpnURLChanged(vpnURL)) {
Optional<JSONObject> json = executeGETRequestJSON(vpnURL + PING_URL_PATH);
cameraAddress = json.map(j -> j.optString("local_url", null))
.map(localURL -> new CameraAddress(vpnURL, localURL));
}
}
return cameraAddress.map(CameraAddress::getLocalURL);
}

private Optional<JSONObject> executeGETRequestJSON(String url) {
try {
return executeGETRequest(url).map(JSONObject::new);
} catch (JSONException e) {
logger.warn("Error on parsing the content as JSON!", e);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A DEBUG log will be sufficient I think.

Copy link
Contributor Author

@Novanic Novanic Jun 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug? When this error occurs, it is an error, the functionality is broken when it occurs. That shouldn't happen. Actually warning isn't enough.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My general rule for logging levels is:

  • trace - used for normal expected execution paths
  • debug - used for unexepected execution paths, but well within normal operation
  • info - used for notable points in an execution path, like a milestone. (in openhab we try reserve this logging level for the core, so bindings should rarely ever call this.)
  • warn - used for notable unexpected execution paths that a regular user (not just a developer) should be notified of. Warnings should be used to indicate that something not-normal occurred and user intervention is required to resolve. Warnings do not indicate a failure to operate merely an abnormal condition of operation that can still be handled by the binding. Failures in binding operation should be indicated by changing the thing status to offline.
  • error - used to indicate catastrophic program failure. This should be used to indicate a catastrophic failure in openhab's ability to operate. A failure in a binding would never cause openhab as a whole to fail so a failure in a bindings should never log an error. Instead that failure should be indicated by changing the thing status.

Now if a JSONException does occur here it doesn't really reflect a failure in the binding so much a failure in an external system and can be thought of more or less akin to connection loss. If you think that such a json parsing failure should be elevated above "warn" then I suggest changing the thing status to OFFLINE to reflect that.

}
return Optional.empty();
}

protected Optional<String> executeGETRequest(String url) {
try {
String content = HttpUtil.executeUrl("GET", url, 5000);
if (content != null && !content.isEmpty()) {
return Optional.of(content);
}
} catch (IOException e) {
logger.warn("Error on accessing local camera url!", e);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A DEBUG log will be sufficient I think.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that such an exception should probably be elevated to the caller since I don't think that simple logging would be how all callers would like this particular issue to be handled.

}
return Optional.empty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
* @author Gaël L'hopital - Initial contribution
*/
public class NetatmoModuleHandler<MODULE> extends AbstractNetatmoThingHandler {
private Logger logger = LoggerFactory.getLogger(NetatmoModuleHandler.class);
private final Logger logger = LoggerFactory.getLogger(NetatmoModuleHandler.class);
private ScheduledFuture<?> refreshJob;
@Nullable
protected MODULE module;
Expand Down Expand Up @@ -131,4 +131,8 @@ protected boolean isRefreshRequired() {
protected void setRefreshRequired(boolean refreshRequired) {
this.refreshRequired = refreshRequired;
}

protected @NonNull Optional<MODULE> getModule() {
return Optional.ofNullable(module);
}
}
Loading