Skip to content

Commit

Permalink
[netatmo] Add support for the floodlight of the Presence camera (open…
Browse files Browse the repository at this point in the history
…hab#7927)

* [openhab#7912] Support for the floodlight of the Presence camera
- Support for the floodlight of the Presence camera added (There are 2 new switches to set the floodlight auto-mode and to switch the floodlight on and off/auto).
- Netatmo API swagger spec updated
- Tests added
* Exception handling and logging corrected
* Potential crash fixed which could occur when a wrong JSON response is returned by the ping command request (when it is a valid JSON but without the expected attribute "local_url").

Signed-off-by: Sven Strohschein <[email protected]>
Signed-off-by: Daan Meijer <[email protected]>
  • Loading branch information
Novanic authored and DaanMeijer committed Sep 1, 2020
1 parent 8705f90 commit daf43c9
Show file tree
Hide file tree
Showing 10 changed files with 643 additions and 27 deletions.
49 changes: 28 additions & 21 deletions bundles/org.openhab.binding.netatmo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -502,34 +502,41 @@ All these channels are read only.

### Welcome and Presence Camera

All these channels are read only.

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.
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.
So to get a refreshed picture, you need to use the refresh parameter in your sitemap image element.

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

| Channel ID | Item Type | Description |
|-----------------------------|-----------|----------------------------------------------------------|
| welcomeCameraStatus | Switch | State of the camera |
| welcomeCameraSdStatus | Switch | State of the SD card |
| welcomeCameraAlimStatus | Switch | State of the power connector |
| welcomeCameraIsLocal | Switch | indicates whether the camera is on the same network than the openHAB Netatmo Binding |
| welcomeCameraLivePicture | Image | Camera Live Snapshot |
| welcomeCameraLivePictureUrl | String | Url of the live snapshot for this camera |
| welcomeCameraLiveStreamUrl | String | Url of the live stream for this camera |
| Channel ID | Item Type | Read/Write | Description |
|-----------------------------|-----------|------------|--------------------------------------------------------------|
| welcomeCameraStatus | Switch | Read-only | State of the camera |
| 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 |
| welcomeCameraLivePicture | Image | Read-only | Camera Live Snapshot |
| welcomeCameraLivePictureUrl | String | Read-only | Url of the live snapshot for this camera |
| welcomeCameraLiveStreamUrl | String | Read-only | Url of the live stream for this camera |

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

| Channel ID | Item Type | Description |
|-----------------------------|-----------|----------------------------------------------------------|
| cameraStatus | Switch | State of the camera |
| cameraSdStatus | Switch | State of the SD card |
| cameraAlimStatus | Switch | State of the power connector |
| cameraIsLocal | Switch | indicates whether the camera is on the same network than the openHAB Netatmo Binding |
| cameraLivePicture | Image | Camera Live Snapshot |
| cameraLivePictureUrl | String | Url of the live snapshot for this camera |
| cameraLiveStreamUrl | String | Url of the live stream for this camera |
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 |
| 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 |
| cameraLivePicture | Image | Read-only | Camera Live Snapshot |
| cameraLivePictureUrl | String | Read-only | Url of the live snapshot for this camera |
| cameraLiveStreamUrl | String | Read-only | Url of the live stream for this camera |
| cameraFloodlightAutoMode | Switch | Read-write | When set the floodlight gets switched to auto instead of off |
| cameraFloodlight | Switch | Read-write | Switch for the floodlight |


### Welcome Person
Expand Down
2 changes: 1 addition & 1 deletion bundles/org.openhab.binding.netatmo/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>https://raw.githubusercontent.com/cbornet/netatmo-swagger-decl/c95b5f5003d91ac9e73e9c855569263cb9013cc2/spec/swagger.yaml</inputSpec>
<inputSpec>https://raw.githubusercontent.com/cbornet/netatmo-swagger-decl/8ba3583f8d851e0b0c0bb4c5066338fe4898cb11/spec/swagger.yaml</inputSpec>
<language>java</language>
<library>retrofit</library>
<configOptions>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,10 @@ public class NetatmoBindingConstants {
public static final String WELCOME_PICTURE_IMAGEID = "image_id";
public static final String WELCOME_PICTURE_KEY = "key";

// Presence outdoor camera specific channels
public static final String CHANNEL_CAMERA_FLOODLIGHT_AUTO_MODE = "cameraFloodlightAutoMode";
public static final String CHANNEL_CAMERA_FLOODLIGHT = "cameraFloodlight";

// List of all supported physical devices and modules
public static final Set<ThingTypeUID> SUPPORTED_DEVICE_THING_TYPES_UIDS = Stream
.of(MAIN_THING_TYPE, MODULE1_THING_TYPE, MODULE2_THING_TYPE, MODULE3_THING_TYPE, MODULE4_THING_TYPE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.openhab.binding.netatmo.internal.discovery.NetatmoModuleDiscoveryService;
import org.openhab.binding.netatmo.internal.handler.NetatmoBridgeHandler;
import org.openhab.binding.netatmo.internal.homecoach.NAHealthyHomeCoachHandler;
import org.openhab.binding.netatmo.internal.presence.NAPresenceCameraHandler;
import org.openhab.binding.netatmo.internal.station.NAMainHandler;
import org.openhab.binding.netatmo.internal.station.NAModule1Handler;
import org.openhab.binding.netatmo.internal.station.NAModule2Handler;
Expand Down Expand Up @@ -109,8 +110,10 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return new NATherm1Handler(thing, stateDescriptionProvider, timeZoneProvider);
} else if (thingTypeUID.equals(WELCOME_HOME_THING_TYPE)) {
return new NAWelcomeHomeHandler(thing, timeZoneProvider);
} else if (thingTypeUID.equals(WELCOME_CAMERA_THING_TYPE) || thingTypeUID.equals(PRESENCE_CAMERA_THING_TYPE)) {
} else if (thingTypeUID.equals(WELCOME_CAMERA_THING_TYPE)) {
return new NAWelcomeCameraHandler(thing, timeZoneProvider);
} else if (thingTypeUID.equals(PRESENCE_CAMERA_THING_TYPE)) {
return new NAPresenceCameraHandler(thing, timeZoneProvider);
} else if (thingTypeUID.equals(WELCOME_PERSON_THING_TYPE)) {
return new NAWelcomePersonHandler(thing, timeZoneProvider);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@
* NAWelcomeCameraHandler)
*
*/
public class CameraHandler extends NetatmoModuleHandler<NAWelcomeCamera> {
public abstract class CameraHandler extends NetatmoModuleHandler<NAWelcomeCamera> {

private static final String LIVE_PICTURE = "/live/snapshot_720.jpg";

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

Expand Down Expand Up @@ -133,7 +133,7 @@ private String getLiveStreamURL() {
}

@SuppressWarnings("null")
private String getVpnUrl() {
protected String getVpnUrl() {
return (module == null) ? null : module.getVpnUrl();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* 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.netatmo.internal.presence;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;

import java.util.Objects;

/**
* {@link CameraAddress} handles the data to address a camera (VPN and local address).
*
* @author Sven Strohschein
*/
@NonNullByDefault
public class CameraAddress {

private final String vpnURL;
private final String localURL;

CameraAddress(final String vpnURL, final String localURL) {
this.vpnURL = vpnURL;
this.localURL = localURL;
}

public String getVpnURL() {
return vpnURL;
}

public String getLocalURL() {
return localURL;
}

/**
* Checks if the VPN URL was changed / isn't equal to the given VPN-URL.
* @param vpnURL old / known VPN URL
* @return true, when the VPN URL isn't equal given VPN URL, otherwise false
*/
public boolean isVpnURLChanged(String vpnURL) {
return !getVpnURL().equals(vpnURL);
}

@Override
public boolean equals(@Nullable Object object) {
if (this == object) return true;
if (object == null || getClass() != object.getClass()) return false;
CameraAddress that = (CameraAddress) object;
return vpnURL.equals(that.vpnURL) && localURL.equals(that.localURL);
}

@Override
public int hashCode() {
return Objects.hash(vpnURL, localURL);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/**
* 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.netatmo.internal.presence;

import static org.openhab.binding.netatmo.internal.ChannelTypeUtils.toOnOffType;

import io.swagger.client.model.NAWelcomeCamera;
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.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.camera.CameraHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.CHANNEL_CAMERA_FLOODLIGHT;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.CHANNEL_CAMERA_FLOODLIGHT_AUTO_MODE;

/**
* {@link NAPresenceCameraHandler} is the class used to handle Presence camera data
*
* @author Sven Strohschein
*/
@NonNullByDefault
public class NAPresenceCameraHandler extends CameraHandler {

private static final String PING_URL_PATH = "/command/ping";
private static final String FLOODLIGHT_SET_URL_PATH = "/command/floodlight_set_config";

private final Logger logger = LoggerFactory.getLogger(NAPresenceCameraHandler.class);

private Optional<CameraAddress> cameraAddress = Optional.empty();
private State floodlightAutoModeState = UnDefType.UNDEF;

public NAPresenceCameraHandler(final 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_FLOODLIGHT:
if (command == OnOffType.ON) {
switchFloodlight(true);
} else if (command == OnOffType.OFF) {
switchFloodlight(false);
}
break;
case CHANNEL_CAMERA_FLOODLIGHT_AUTO_MODE:
if (command == OnOffType.ON) {
switchFloodlightAutoMode(true);
} else if (command == OnOffType.OFF) {
switchFloodlightAutoMode(false);
}
break;
}
super.handleCommand(channelUID, command);
}

@Override
protected State getNAThingProperty(@NonNull String channelId) {
switch (channelId) {
case CHANNEL_CAMERA_FLOODLIGHT:
return getFloodlightState();
case CHANNEL_CAMERA_FLOODLIGHT_AUTO_MODE:
//The auto-mode state shouldn't be updated, because this isn't a dedicated information. When the
// floodlight is switched on the state within the Netatmo API is "on" and the information if the previous
// state was "auto" instead of "off" is lost... Therefore the binding handles its own auto-mode state.
if (floodlightAutoModeState == UnDefType.UNDEF) {
floodlightAutoModeState = getFloodlightAutoModeState();
}
return floodlightAutoModeState;
}
return super.getNAThingProperty(channelId);
}

private State getFloodlightState() {
if (module != null) {
final boolean isOn = module.getLightModeStatus() == NAWelcomeCamera.LightModeStatusEnum.ON;
return toOnOffType(isOn);
}
return UnDefType.UNDEF;
}

private State getFloodlightAutoModeState() {
if (module != null) {
return toOnOffType(module.getLightModeStatus() == NAWelcomeCamera.LightModeStatusEnum.AUTO);
}
return UnDefType.UNDEF;
}

private void switchFloodlight(boolean isOn) {
if (isOn) {
changeFloodlightMode(NAWelcomeCamera.LightModeStatusEnum.ON);
} else {
switchFloodlightAutoMode(floodlightAutoModeState == OnOffType.ON);
}
}

private void switchFloodlightAutoMode(boolean isAutoMode) {
floodlightAutoModeState = toOnOffType(isAutoMode);
if (isAutoMode) {
changeFloodlightMode(NAWelcomeCamera.LightModeStatusEnum.AUTO);
} else {
changeFloodlightMode(NAWelcomeCamera.LightModeStatusEnum.OFF);
}
}

private void changeFloodlightMode(NAWelcomeCamera.LightModeStatusEnum mode) {
Optional<String> localCameraURL = getLocalCameraURL();
if (localCameraURL.isPresent()) {
String url = localCameraURL.get()
+ FLOODLIGHT_SET_URL_PATH
+ "?config=%7B%22mode%22:%22"
+ mode.toString()
+ "%22%7D";
executeGETRequest(url);
}
}

private Optional<String> getLocalCameraURL() {
String vpnURL = getVpnUrl();
if (vpnURL != null) {
//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();
}

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 @@ -32,6 +32,7 @@
import org.eclipse.smarthome.core.types.UnDefType;
import org.eclipse.smarthome.io.net.http.HttpUtil;
import org.openhab.binding.netatmo.internal.ChannelTypeUtils;
import org.openhab.binding.netatmo.internal.camera.CameraHandler;
import org.openhab.binding.netatmo.internal.handler.AbstractNetatmoThingHandler;
import org.openhab.binding.netatmo.internal.handler.NetatmoDeviceHandler;
import org.openhab.binding.netatmo.internal.webhook.NAWebhookCameraEvent;
Expand Down Expand Up @@ -145,7 +146,7 @@ protected State getNAThingProperty(String channelId) {
String cameraId = lastEvent.get().getCameraId();
Optional<AbstractNetatmoThingHandler> thing = getBridgeHandler().findNAThing(cameraId);
if (thing.isPresent()) {
NAWelcomeCameraHandler eventCamera = (NAWelcomeCameraHandler) thing.get();
CameraHandler eventCamera = (CameraHandler) thing.get();
String streamUrl = eventCamera.getStreamURL(lastEvent.get().getVideoId());
if (streamUrl != null) {
return new StringType(streamUrl);
Expand Down
Loading

0 comments on commit daf43c9

Please sign in to comment.