From 9b1404d43799bd5999b6e93e765e98b6125d07f8 Mon Sep 17 00:00:00 2001 From: Sven Strohschein Date: Sun, 28 Jun 2020 22:29:13 +0200 Subject: [PATCH] [netatmo] Support for switching video surveillance on/off (#7968) * [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 Signed-off-by: MPH80 --- bundles/org.openhab.binding.netatmo/README.md | 11 +- .../{presence => camera}/CameraAddress.java | 2 +- .../internal/camera/CameraHandler.java | 148 +++++++++++++----- .../handler/NetatmoModuleHandler.java | 6 +- .../presence/NAPresenceCameraHandler.java | 61 +------- .../welcome/NAWelcomeHomeHandler.java | 6 +- .../main/resources/ESH-INF/thing/camera.xml | 1 - .../presence/NAPresenceCameraHandlerTest.java | 65 ++++++-- .../welcome/NAWelcomeHomeHandlerTest.java | 127 ++++++++++++--- 9 files changed, 290 insertions(+), 137 deletions(-) rename bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/{presence => camera}/CameraAddress.java (96%) diff --git a/bundles/org.openhab.binding.netatmo/README.md b/bundles/org.openhab.binding.netatmo/README.md index 3c9bff21af24c..0bc2b43e47bc0 100644 --- a/bundles/org.openhab.binding.netatmo/README.md +++ b/bundles/org.openhab.binding.netatmo/README.md @@ -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 | @@ -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 | diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/presence/CameraAddress.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/camera/CameraAddress.java similarity index 96% rename from bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/presence/CameraAddress.java rename to bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/camera/CameraAddress.java index ce55a1a5c3045..af54e36fa5e7a 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/presence/CameraAddress.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/camera/CameraAddress.java @@ -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; diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/camera/CameraHandler.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/camera/CameraHandler.java index 2c84de4eff119..7d152ee466dde 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/camera/CameraHandler.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/camera/CameraHandler.java @@ -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 @@ -33,20 +44,42 @@ * NAWelcomeCameraHandler) * */ +@NonNullByDefault public abstract class CameraHandler extends NetatmoModuleHandler { + 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 = 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) { @@ -69,34 +102,32 @@ 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 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); } /** @@ -104,12 +135,8 @@ protected State getLiveStreamState() { * * @return Url of the live snapshot */ - private String getLivePictureURL() { - String result = getVpnUrl(); - if (result != null) { - result += LIVE_PICTURE; - } - return result; + private Optional getLivePictureURL() { + return getVpnUrl().map(u -> u += LIVE_PICTURE); } /** @@ -117,33 +144,32 @@ private String getLivePictureURL() { * * @return Url of the live stream */ - private String getLiveStreamURL() { - String result = getVpnUrl(); - if (result == null) { - return null; + private Optional getLiveStreamURL() { + Optional 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 getVpnUrl() { + return getModule().map(NAWelcomeCamera::getVpnUrl); } - public String getStreamURL(String videoId) { - String result = getVpnUrl(); - if (result == null) { - return null; + public Optional getStreamURL(String videoId) { + Optional 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"); @@ -151,11 +177,61 @@ public String getStreamURL(String videoId) { 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 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 getLocalCameraURL() { + Optional 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 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 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 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(); } } diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/NetatmoModuleHandler.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/NetatmoModuleHandler.java index d6a6b963c1a05..7d59e1bf9d67d 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/NetatmoModuleHandler.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/NetatmoModuleHandler.java @@ -38,7 +38,7 @@ * @author Gaƫl L'hopital - Initial contribution */ public class NetatmoModuleHandler extends AbstractNetatmoThingHandler { - private Logger logger = LoggerFactory.getLogger(NetatmoModuleHandler.class); + private final Logger logger = LoggerFactory.getLogger(NetatmoModuleHandler.class); private ScheduledFuture refreshJob; @Nullable protected MODULE module; @@ -131,4 +131,8 @@ protected boolean isRefreshRequired() { protected void setRefreshRequired(boolean refreshRequired) { this.refreshRequired = refreshRequired; } + + protected @NonNull Optional getModule() { + return Optional.ofNullable(module); + } } diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandler.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandler.java index 133240b1d54f2..5c0250b852c6a 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandler.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandler.java @@ -24,14 +24,8 @@ 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; @@ -40,17 +34,13 @@ /** * {@link NAPresenceCameraHandler} is the class used to handle Presence camera data * - * @author Sven Strohschein + * @author Sven Strohschein - Initial contribution */ @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 = Optional.empty(); private State floodlightAutoModeState = UnDefType.UNDEF; public NAPresenceCameraHandler(final Thing thing, final TimeZoneProvider timeZoneProvider) { @@ -97,18 +87,15 @@ protected State getNAThingProperty(@NonNull String channelId) { } private State getFloodlightState() { - if (module != null) { - final boolean isOn = module.getLightModeStatus() == NAWelcomeCamera.LightModeStatusEnum.ON; - return toOnOffType(isOn); - } - return UnDefType.UNDEF; + return getModule() + .map(m -> toOnOffType(m.getLightModeStatus() == NAWelcomeCamera.LightModeStatusEnum.ON)) + .orElse(UnDefType.UNDEF); } private State getFloodlightAutoModeState() { - if (module != null) { - return toOnOffType(module.getLightModeStatus() == NAWelcomeCamera.LightModeStatusEnum.AUTO); - } - return UnDefType.UNDEF; + return getModule() + .map(m -> toOnOffType(m.getLightModeStatus() == NAWelcomeCamera.LightModeStatusEnum.AUTO)) + .orElse(UnDefType.UNDEF); } private void switchFloodlight(boolean isOn) { @@ -137,40 +124,8 @@ private void changeFloodlightMode(NAWelcomeCamera.LightModeStatusEnum mode) { + mode.toString() + "%22%7D"; executeGETRequest(url); - } - } - - private Optional 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 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 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 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); + invalidateParentCacheAndRefresh(); } - return Optional.empty(); } } diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandler.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandler.java index 19b4ba0b81557..b37ee8134bc0c 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandler.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandler.java @@ -147,9 +147,9 @@ protected State getNAThingProperty(String channelId) { Optional thing = getBridgeHandler().findNAThing(cameraId); if (thing.isPresent()) { CameraHandler eventCamera = (CameraHandler) thing.get(); - String streamUrl = eventCamera.getStreamURL(lastEvent.get().getVideoId()); - if (streamUrl != null) { - return new StringType(streamUrl); + Optional streamUrl = eventCamera.getStreamURL(lastEvent.get().getVideoId()); + if (streamUrl.isPresent()) { + return new StringType(streamUrl.get()); } } } diff --git a/bundles/org.openhab.binding.netatmo/src/main/resources/ESH-INF/thing/camera.xml b/bundles/org.openhab.binding.netatmo/src/main/resources/ESH-INF/thing/camera.xml index be0698f6e5af5..d834b66ea5377 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/resources/ESH-INF/thing/camera.xml +++ b/bundles/org.openhab.binding.netatmo/src/main/resources/ESH-INF/thing/camera.xml @@ -56,7 +56,6 @@ Switch State of the camera - diff --git a/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandlerTest.java b/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandlerTest.java index 977f754e232f8..d654c957b6723 100644 --- a/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandlerTest.java +++ b/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/presence/NAPresenceCameraHandlerTest.java @@ -37,7 +37,7 @@ import static org.mockito.Mockito.*; /** - * @author Sven Strohschein + * @author Sven Strohschein - Initial contribution */ @RunWith(MockitoJUnitRunner.class) public class NAPresenceCameraHandlerTest { @@ -53,6 +53,7 @@ public class NAPresenceCameraHandlerTest { private Thing presenceCameraThing; private NAWelcomeCamera presenceCamera; + private ChannelUID cameraStatusChannelUID; private ChannelUID floodlightChannelUID; private ChannelUID floodlightAutoModeChannelUID; private NAPresenceCameraHandlerAccessible handler; @@ -62,12 +63,50 @@ public void before() { presenceCameraThing = new ThingImpl(new ThingTypeUID("netatmo", "NOC"), "1"); presenceCamera = new NAWelcomeCamera(); + cameraStatusChannelUID = new ChannelUID(presenceCameraThing.getUID(), NetatmoBindingConstants.CHANNEL_CAMERA_STATUS); floodlightChannelUID = new ChannelUID(presenceCameraThing.getUID(), NetatmoBindingConstants.CHANNEL_CAMERA_FLOODLIGHT); floodlightAutoModeChannelUID = new ChannelUID(presenceCameraThing.getUID(), NetatmoBindingConstants.CHANNEL_CAMERA_FLOODLIGHT_AUTO_MODE); handler = new NAPresenceCameraHandlerAccessible(presenceCameraThing, presenceCamera); } + @Test + public void testHandleCommand_Switch_Surveillance_on() { + when(requestExecutorMock.executeGETRequest(DUMMY_VPN_URL + "/command/ping")).thenReturn(DUMMY_PING_RESPONSE); + + presenceCamera.setVpnUrl(DUMMY_VPN_URL); + handler.handleCommand(cameraStatusChannelUID, OnOffType.ON); + + verify(requestExecutorMock, times(2)).executeGETRequest(any()); //1.) execute ping + 2.) execute switch on + verify(requestExecutorMock).executeGETRequest(DUMMY_LOCAL_URL + "/command/changestatus?status=on"); + } + + @Test + public void testHandleCommand_Switch_Surveillance_off() { + when(requestExecutorMock.executeGETRequest(DUMMY_VPN_URL + "/command/ping")).thenReturn(DUMMY_PING_RESPONSE); + + presenceCamera.setVpnUrl(DUMMY_VPN_URL); + handler.handleCommand(cameraStatusChannelUID, OnOffType.OFF); + + verify(requestExecutorMock, times(2)).executeGETRequest(any()); //1.) execute ping + 2.) execute switch off + verify(requestExecutorMock).executeGETRequest(DUMMY_LOCAL_URL + "/command/changestatus?status=off"); + } + + @Test + public void testHandleCommand_Switch_Surveillance_unknown_command() { + presenceCamera.setVpnUrl(DUMMY_VPN_URL); + handler.handleCommand(cameraStatusChannelUID, RefreshType.REFRESH); + + verify(requestExecutorMock, never()).executeGETRequest(any()); //nothing should get executed on a refresh command + } + + @Test + public void testHandleCommand_Switch_Surveillance_without_VPN() { + handler.handleCommand(cameraStatusChannelUID, OnOffType.ON); + + verify(requestExecutorMock, never()).executeGETRequest(any()); //nothing should get executed when no VPN address is set + } + @Test public void testHandleCommand_Switch_Floodlight_on() { when(requestExecutorMock.executeGETRequest(DUMMY_VPN_URL + "/command/ping")).thenReturn(DUMMY_PING_RESPONSE); @@ -334,9 +373,9 @@ public void testGetNAThingProperty_FloodlightAutoMode_Module_NULL() { @Test public void testGetStreamURL() { presenceCamera.setVpnUrl(DUMMY_VPN_URL); - String streamURL = handler.getStreamURL("dummyVideoId"); - assertNotNull(streamURL); - assertEquals(DUMMY_VPN_URL + "/vod/dummyVideoId/index.m3u8", streamURL); + Optional streamURL = handler.getStreamURL("dummyVideoId"); + assertTrue(streamURL.isPresent()); + assertEquals(DUMMY_VPN_URL + "/vod/dummyVideoId/index.m3u8", streamURL.get()); } @Test @@ -344,9 +383,9 @@ public void testGetStreamURL_local() { presenceCamera.setVpnUrl(DUMMY_VPN_URL); presenceCamera.setIsLocal(true); - String streamURL = handler.getStreamURL("dummyVideoId"); - assertNotNull(streamURL); - assertEquals(DUMMY_VPN_URL + "/vod/dummyVideoId/index_local.m3u8", streamURL); + Optional streamURL = handler.getStreamURL("dummyVideoId"); + assertTrue(streamURL.isPresent()); + assertEquals(DUMMY_VPN_URL + "/vod/dummyVideoId/index_local.m3u8", streamURL.get()); } @Test @@ -354,15 +393,15 @@ public void testGetStreamURL_not_local() { presenceCamera.setVpnUrl(DUMMY_VPN_URL); presenceCamera.setIsLocal(false); - String streamURL = handler.getStreamURL("dummyVideoId"); - assertNotNull(streamURL); - assertEquals(DUMMY_VPN_URL + "/vod/dummyVideoId/index.m3u8", streamURL); + Optional streamURL = handler.getStreamURL("dummyVideoId"); + assertTrue(streamURL.isPresent()); + assertEquals(DUMMY_VPN_URL + "/vod/dummyVideoId/index.m3u8", streamURL.get()); } @Test public void testGetStreamURL_without_VPN() { - String streamURL = handler.getStreamURL("dummyVideoId"); - assertNull(streamURL); + Optional streamURL = handler.getStreamURL("dummyVideoId"); + assertFalse(streamURL.isPresent()); } @Test @@ -404,7 +443,7 @@ private interface RequestExecutor { private class NAPresenceCameraHandlerAccessible extends NAPresenceCameraHandler { - public NAPresenceCameraHandlerAccessible(Thing thing, NAWelcomeCamera presenceCamera) { + private NAPresenceCameraHandlerAccessible(Thing thing, NAWelcomeCamera presenceCamera) { super(thing, timeZoneProviderMock); module = presenceCamera; } diff --git a/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandlerTest.java b/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandlerTest.java index 683e77392e1f4..1ecd81ee5bd10 100644 --- a/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandlerTest.java +++ b/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/welcome/NAWelcomeHomeHandlerTest.java @@ -15,6 +15,8 @@ import io.swagger.client.model.NAWelcomeEvent; import io.swagger.client.model.NAWelcomeHome; import io.swagger.client.model.NAWelcomeHomeData; +import io.swagger.client.model.NAWelcomeSubEvent; +import org.eclipse.jdt.annotation.NonNull; import org.eclipse.smarthome.core.i18n.TimeZoneProvider; import org.eclipse.smarthome.core.library.types.StringType; import org.eclipse.smarthome.core.thing.Thing; @@ -31,14 +33,13 @@ import org.openhab.binding.netatmo.internal.webhook.NAWebhookCameraEvent; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import static org.junit.Assert.*; import static org.mockito.Mockito.*; /** - * @author Sven Strohschein + * @author Sven Strohschein - Initial contribution */ @RunWith(MockitoJUnitRunner.class) public class NAWelcomeHomeHandlerTest { @@ -48,35 +49,20 @@ public class NAWelcomeHomeHandlerTest { @Mock private TimeZoneProvider timeZoneProviderMock; private Thing welcomeHomeThing; - private NAWelcomeHomeHandler handler; + private NAWelcomeHomeHandlerAccessible handler; @Mock private NetatmoBridgeHandler bridgeHandlerMock; @Before public void before() { welcomeHomeThing = new ThingImpl(new ThingTypeUID("netatmo", "NAWelcomeHome"), "1"); - handler = new NAWelcomeHomeHandler(welcomeHomeThing, timeZoneProviderMock) { - @Override - protected NetatmoBridgeHandler getBridgeHandler() { - return bridgeHandlerMock; - } - - @Override - protected String getId() { - return DUMMY_HOME_ID; - } - }; + handler = new NAWelcomeHomeHandlerAccessible(welcomeHomeThing); } @Test public void testUpdateReadings_with_Events() { - NAWelcomeEvent event_1 = new NAWelcomeEvent(); - event_1.setType(NAWebhookCameraEvent.EventTypeEnum.PERSON.toString()); - event_1.setTime(1592661881); - - NAWelcomeEvent event_2 = new NAWelcomeEvent(); - event_2.setType(NAWebhookCameraEvent.EventTypeEnum.MOVEMENT.toString()); - event_2.setTime(1592661882); + NAWelcomeEvent event_1 = createEvent(1592661881, NAWebhookCameraEvent.EventTypeEnum.PERSON); + NAWelcomeEvent event_2 = createEvent(1592661882, NAWebhookCameraEvent.EventTypeEnum.MOVEMENT); NAWelcomeHome home = new NAWelcomeHome(); home.setId(DUMMY_HOME_ID); @@ -95,13 +81,11 @@ public void testUpdateReadings_with_Events() { home.setEvents(Arrays.asList(event_2, event_1)); //the second (last) event is still expected (independent from the order of these are added) assertEquals(new StringType("movement"), handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); - } @Test public void testUpdateReadings_with_1_Event() { - NAWelcomeEvent event = new NAWelcomeEvent(); - event.setType(NAWebhookCameraEvent.EventTypeEnum.PERSON.toString()); + NAWelcomeEvent event = createEvent(1592661881, NAWebhookCameraEvent.EventTypeEnum.PERSON); NAWelcomeHome home = new NAWelcomeHome(); home.setId(DUMMY_HOME_ID); @@ -147,4 +131,99 @@ public void testUpdateReadings_no_HomeData() { assertEquals(UnDefType.UNDEF, handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); } + + @Test + public void testTriggerChannelIfRequired() { + NAWelcomeEvent event_1 = createPresenceEvent(1592661881, NAWelcomeSubEvent.TypeEnum.ANIMAL); + NAWelcomeEvent event_2 = createPresenceEvent(1592661882, NAWelcomeSubEvent.TypeEnum.HUMAN); + NAWelcomeEvent event_3 = createEvent(1592661883, NAWebhookCameraEvent.EventTypeEnum.MOVEMENT); + + NAWelcomeHome home = new NAWelcomeHome(); + home.setId(DUMMY_HOME_ID); + home.setEvents(Collections.singletonList(event_1)); + + NAWelcomeHomeData homeData = new NAWelcomeHomeData(); + homeData.setHomes(Collections.singletonList(home)); + + when(bridgeHandlerMock.getWelcomeDataBody(DUMMY_HOME_ID)).thenReturn(homeData); + + handler.updateReadings(); + handler.triggerChannelIfRequired(NetatmoBindingConstants.CHANNEL_CAMERA_EVENT); + + //No triggered event is expected, because the binding is just started (with existing events). + assertEquals(0, handler.getTriggerChannelCount()); + + home.setEvents(Arrays.asList(event_1, event_2)); + + handler.updateReadings(); + handler.triggerChannelIfRequired(NetatmoBindingConstants.CHANNEL_CAMERA_EVENT); + + //1 triggered event is expected, because there is 1 new event since binding start (outdoor / detected human). + assertEquals(1, handler.getTriggerChannelCount()); + assertEquals(new StringType("outdoor"), handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); + + home.setEvents(Arrays.asList(event_1, event_2)); + + handler.updateReadings(); + handler.triggerChannelIfRequired(NetatmoBindingConstants.CHANNEL_CAMERA_EVENT); + + //No new triggered event is expected, because there are still the same events as before the refresh. + assertEquals(1, handler.getTriggerChannelCount()); + assertEquals(new StringType("outdoor"), handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); + + home.setEvents(Arrays.asList(event_1, event_2, event_3)); + + handler.updateReadings(); + handler.triggerChannelIfRequired(NetatmoBindingConstants.CHANNEL_CAMERA_EVENT); + + //1 new triggered event is expected (2 in sum), because there is 1 new event since the last triggered event (movement after outdoor / detected human). + assertEquals(2, handler.getTriggerChannelCount()); + assertEquals(new StringType("movement"), handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE)); + } + + private static NAWelcomeEvent createPresenceEvent(int eventTime, NAWelcomeSubEvent.TypeEnum detectedObjectType) { + NAWelcomeSubEvent subEvent = new NAWelcomeSubEvent(); + subEvent.setTime(eventTime); + subEvent.setType(detectedObjectType); + + NAWelcomeEvent event = createEvent(eventTime, NAWebhookCameraEvent.EventTypeEnum.OUTDOOR); + event.setEventList(Collections.singletonList(subEvent)); + return event; + } + + private static NAWelcomeEvent createEvent(int eventTime, NAWebhookCameraEvent.EventTypeEnum eventType) { + NAWelcomeEvent event = new NAWelcomeEvent(); + event.setType(eventType.toString()); + event.setTime(eventTime); + return event; + } + + private class NAWelcomeHomeHandlerAccessible extends NAWelcomeHomeHandler { + + private int triggerChannelCount; + + private NAWelcomeHomeHandlerAccessible(Thing thing) { + super(thing, timeZoneProviderMock); + } + + @Override + protected NetatmoBridgeHandler getBridgeHandler() { + return bridgeHandlerMock; + } + + @Override + protected String getId() { + return DUMMY_HOME_ID; + } + + @Override + protected void triggerChannel(@NonNull String channelID, @NonNull String event) { + triggerChannelCount++; + super.triggerChannel(channelID, event); + } + + private int getTriggerChannelCount() { + return triggerChannelCount; + } + } }