Skip to content

Commit

Permalink
[netatmo] Support for switching video surveillance on/off (openhab#7968)
Browse files Browse the repository at this point in the history
* [7938] Support for switching video surveillance on/off
- The video surveillance can now get switched on/off
- When the command is executed (floodlight or video surveillance switched on/off) a refresh is executed after 2 seconds to update all channels
* Warnings fixed
* New tests for isNewLastEvent added

Signed-off-by: Sven Strohschein <[email protected]>
Signed-off-by: CSchlipp <[email protected]>
  • Loading branch information
Novanic authored and CSchlipp committed Jul 26, 2020
1 parent 6b01ab0 commit ea226fb
Show file tree
Hide file tree
Showing 9 changed files with 290 additions and 137 deletions.
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);
}
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);
}
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

0 comments on commit ea226fb

Please sign in to comment.