Codebase as of c53e4aed26 as an initial commit for the shrunk repo

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2010-02-20 19:23:32 +01:00
committed by Kai Kreuzer
commit bbf1a7fd29
302 changed files with 29726 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
/**
* 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.nest.handler;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import static org.mockito.MockitoAnnotations.openMocks;
import javax.ws.rs.client.ClientBuilder;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.openhab.binding.nest.internal.config.NestBridgeConfiguration;
import org.openhab.binding.nest.internal.handler.NestBridgeHandler;
import org.openhab.binding.nest.internal.handler.NestRedirectUrlSupplier;
import org.openhab.binding.nest.test.NestTestBridgeHandler;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.osgi.service.jaxrs.client.SseEventSourceFactory;
/**
* Tests cases for {@link NestBridgeHandler}.
*
* @author David Bennett - Initial contribution
*/
public class NestBridgeHandlerTest {
private ThingHandler handler;
private AutoCloseable mocksCloseable;
private @Mock Bridge bridge;
private @Mock ThingHandlerCallback callback;
private @Mock ClientBuilder clientBuilder;
private @Mock Configuration configuration;
private @Mock SseEventSourceFactory eventSourceFactory;
private @Mock NestRedirectUrlSupplier redirectUrlSupplier;
@BeforeEach
public void beforeEach() {
mocksCloseable = openMocks(this);
handler = new NestTestBridgeHandler(bridge, clientBuilder, eventSourceFactory, "http://localhost");
handler.setCallback(callback);
}
@AfterEach
public void afterEach() throws Exception {
mocksCloseable.close();
}
@SuppressWarnings("null")
@Test
public void initializeShouldCallTheCallback() {
when(bridge.getConfiguration()).thenReturn(configuration);
NestBridgeConfiguration bridgeConfig = new NestBridgeConfiguration();
when(configuration.as(eq(NestBridgeConfiguration.class))).thenReturn(bridgeConfig);
bridgeConfig.accessToken = "my token";
// we expect the handler#initialize method to call the callback during execution and
// pass it the thing and a ThingStatusInfo object containing the ThingStatus of the thing.
handler.initialize();
// the argument captor will capture the argument of type ThingStatusInfo given to the
// callback#statusUpdated method.
ArgumentCaptor<ThingStatusInfo> statusInfoCaptor = ArgumentCaptor.forClass(ThingStatusInfo.class);
// verify the interaction with the callback and capture the ThingStatusInfo argument:
verify(callback).statusUpdated(eq(bridge), statusInfoCaptor.capture());
// assert that the ThingStatusInfo given to the callback was build with the UNKNOWN status:
ThingStatusInfo thingStatusInfo = statusInfoCaptor.getValue();
assertThat(thingStatusInfo.getStatus(), is(equalTo(ThingStatus.UNKNOWN)));
}
}

View File

@@ -0,0 +1,149 @@
/**
* 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.nest.handler;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.openhab.binding.nest.internal.NestBindingConstants.*;
import static org.openhab.binding.nest.internal.data.NestDataUtil.*;
import static org.openhab.core.library.types.OnOffType.*;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.openhab.binding.nest.internal.config.NestDeviceConfiguration;
import org.openhab.binding.nest.internal.handler.NestCameraHandler;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.builder.ThingBuilder;
/**
* Tests for {@link NestCameraHandler}.
*
* @author Wouter Born - Increase test coverage
*/
public class NestCameraHandlerTest extends NestThingHandlerOSGiTest {
private static final ThingUID CAMERA_UID = new ThingUID(THING_TYPE_CAMERA, "camera1");
private static final int CHANNEL_COUNT = 20;
public NestCameraHandlerTest() {
super(NestCameraHandler.class);
}
@Override
protected Thing buildThing(Bridge bridge) {
Map<String, Object> properties = new HashMap<>();
properties.put(NestDeviceConfiguration.DEVICE_ID, CAMERA1_DEVICE_ID);
return ThingBuilder.create(THING_TYPE_CAMERA, CAMERA_UID).withLabel("Test Camera").withBridge(bridge.getUID())
.withChannels(buildChannels(THING_TYPE_CAMERA, CAMERA_UID))
.withConfiguration(new Configuration(properties)).build();
}
@Test
public void completeCameraUpdate() throws IOException {
assertThat(thing.getChannels().size(), is(CHANNEL_COUNT));
assertThat(thing.getStatus(), is(ThingStatus.OFFLINE));
waitForAssert(() -> assertThat(bridge.getStatus(), is(ThingStatus.ONLINE)));
putStreamingEventData(fromFile(COMPLETE_DATA_FILE_NAME));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.ONLINE)));
// Camera channel group
assertThatItemHasState(CHANNEL_CAMERA_APP_URL, new StringType("https://camera_app_url"));
assertThatItemHasState(CHANNEL_CAMERA_AUDIO_INPUT_ENABLED, ON);
assertThatItemHasState(CHANNEL_CAMERA_LAST_ONLINE_CHANGE, parseDateTimeType("2017-01-22T08:19:20.000Z"));
assertThatItemHasState(CHANNEL_CAMERA_PUBLIC_SHARE_ENABLED, OFF);
assertThatItemHasState(CHANNEL_CAMERA_PUBLIC_SHARE_URL, new StringType("https://camera_public_share_url"));
assertThatItemHasState(CHANNEL_CAMERA_SNAPSHOT_URL, new StringType("https://camera_snapshot_url"));
assertThatItemHasState(CHANNEL_CAMERA_STREAMING, OFF);
assertThatItemHasState(CHANNEL_CAMERA_VIDEO_HISTORY_ENABLED, OFF);
assertThatItemHasState(CHANNEL_CAMERA_WEB_URL, new StringType("https://camera_web_url"));
// Last event channel group
assertThatItemHasState(CHANNEL_LAST_EVENT_ACTIVITY_ZONES, new StringType("id1,id2"));
assertThatItemHasState(CHANNEL_LAST_EVENT_ANIMATED_IMAGE_URL,
new StringType("https://last_event_animated_image_url"));
assertThatItemHasState(CHANNEL_LAST_EVENT_APP_URL, new StringType("https://last_event_app_url"));
assertThatItemHasState(CHANNEL_LAST_EVENT_END_TIME, parseDateTimeType("2017-01-22T07:40:38.680Z"));
assertThatItemHasState(CHANNEL_LAST_EVENT_HAS_MOTION, ON);
assertThatItemHasState(CHANNEL_LAST_EVENT_HAS_PERSON, OFF);
assertThatItemHasState(CHANNEL_LAST_EVENT_HAS_SOUND, OFF);
assertThatItemHasState(CHANNEL_LAST_EVENT_IMAGE_URL, new StringType("https://last_event_image_url"));
assertThatItemHasState(CHANNEL_LAST_EVENT_START_TIME, parseDateTimeType("2017-01-22T07:40:19.020Z"));
assertThatItemHasState(CHANNEL_LAST_EVENT_URLS_EXPIRE_TIME, parseDateTimeType("2017-02-05T07:40:19.020Z"));
assertThatItemHasState(CHANNEL_LAST_EVENT_WEB_URL, new StringType("https://last_event_web_url"));
assertThatAllItemStatesAreNotNull();
}
@Test
public void incompleteCameraUpdate() throws IOException {
assertThat(thing.getChannels().size(), is(CHANNEL_COUNT));
assertThat(thing.getStatus(), is(ThingStatus.OFFLINE));
waitForAssert(() -> assertThat(bridge.getStatus(), is(ThingStatus.ONLINE)));
putStreamingEventData(fromFile(COMPLETE_DATA_FILE_NAME));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.ONLINE)));
assertThatAllItemStatesAreNotNull();
putStreamingEventData(fromFile(INCOMPLETE_DATA_FILE_NAME));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.UNKNOWN)));
assertThatAllItemStatesAreNull();
}
@Test
public void cameraGone() throws IOException {
waitForAssert(() -> assertThat(bridge.getStatus(), is(ThingStatus.ONLINE)));
putStreamingEventData(fromFile(COMPLETE_DATA_FILE_NAME));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.ONLINE)));
putStreamingEventData(fromFile(EMPTY_DATA_FILE_NAME));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.OFFLINE)));
assertThat(thing.getStatusInfo().getStatusDetail(), is(ThingStatusDetail.GONE));
}
@Test
public void channelRefresh() throws IOException {
waitForAssert(() -> assertThat(bridge.getStatus(), is(ThingStatus.ONLINE)));
putStreamingEventData(fromFile(COMPLETE_DATA_FILE_NAME));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.ONLINE)));
assertThatAllItemStatesAreNotNull();
updateAllItemStatesToNull();
assertThatAllItemStatesAreNull();
refreshAllChannels();
assertThatAllItemStatesAreNotNull();
}
@Test
public void handleStreamingCommands() throws IOException {
handleCommand(CHANNEL_CAMERA_STREAMING, ON);
assertNestApiPropertyState(CAMERA1_DEVICE_ID, "is_streaming", "true");
handleCommand(CHANNEL_CAMERA_STREAMING, OFF);
assertNestApiPropertyState(CAMERA1_DEVICE_ID, "is_streaming", "false");
handleCommand(CHANNEL_CAMERA_STREAMING, ON);
assertNestApiPropertyState(CAMERA1_DEVICE_ID, "is_streaming", "true");
}
}

View File

@@ -0,0 +1,120 @@
/**
* 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.nest.handler;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.openhab.binding.nest.internal.NestBindingConstants.*;
import static org.openhab.binding.nest.internal.data.NestDataUtil.*;
import static org.openhab.core.library.types.OnOffType.OFF;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.openhab.binding.nest.internal.config.NestDeviceConfiguration;
import org.openhab.binding.nest.internal.handler.NestSmokeDetectorHandler;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.builder.ThingBuilder;
/**
* Tests for {@link NestSmokeDetectorHandler}.
*
* @author Wouter Born - Increase test coverage
*/
public class NestSmokeDetectorHandlerTest extends NestThingHandlerOSGiTest {
private static final ThingUID SMOKE_DETECTOR_UID = new ThingUID(THING_TYPE_SMOKE_DETECTOR, "smoke1");
private static final int CHANNEL_COUNT = 7;
public NestSmokeDetectorHandlerTest() {
super(NestSmokeDetectorHandler.class);
}
@Override
protected Thing buildThing(Bridge bridge) {
Map<String, Object> properties = new HashMap<>();
properties.put(NestDeviceConfiguration.DEVICE_ID, SMOKE1_DEVICE_ID);
return ThingBuilder.create(THING_TYPE_SMOKE_DETECTOR, SMOKE_DETECTOR_UID).withLabel("Test Smoke Detector")
.withBridge(bridge.getUID()).withChannels(buildChannels(THING_TYPE_SMOKE_DETECTOR, SMOKE_DETECTOR_UID))
.withConfiguration(new Configuration(properties)).build();
}
@Test
public void completeSmokeDetectorUpdate() throws IOException {
assertThat(thing.getChannels().size(), is(CHANNEL_COUNT));
assertThat(thing.getStatus(), is(ThingStatus.OFFLINE));
waitForAssert(() -> assertThat(bridge.getStatus(), is(ThingStatus.ONLINE)));
putStreamingEventData(fromFile(COMPLETE_DATA_FILE_NAME));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.ONLINE)));
assertThatItemHasState(CHANNEL_CO_ALARM_STATE, new StringType("OK"));
assertThatItemHasState(CHANNEL_LAST_CONNECTION, parseDateTimeType("2017-02-02T20:53:05.338Z"));
assertThatItemHasState(CHANNEL_LAST_MANUAL_TEST_TIME, parseDateTimeType("2016-10-31T23:59:59.000Z"));
assertThatItemHasState(CHANNEL_LOW_BATTERY, OFF);
assertThatItemHasState(CHANNEL_MANUAL_TEST_ACTIVE, OFF);
assertThatItemHasState(CHANNEL_SMOKE_ALARM_STATE, new StringType("OK"));
assertThatItemHasState(CHANNEL_UI_COLOR_STATE, new StringType("GREEN"));
assertThatAllItemStatesAreNotNull();
}
@Test
public void incompleteSmokeDetectorUpdate() throws IOException {
assertThat(thing.getChannels().size(), is(CHANNEL_COUNT));
assertThat(thing.getStatus(), is(ThingStatus.OFFLINE));
waitForAssert(() -> assertThat(bridge.getStatus(), is(ThingStatus.ONLINE)));
putStreamingEventData(fromFile(COMPLETE_DATA_FILE_NAME));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.ONLINE)));
assertThatAllItemStatesAreNotNull();
putStreamingEventData(fromFile(INCOMPLETE_DATA_FILE_NAME));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.UNKNOWN)));
assertThatAllItemStatesAreNull();
}
@Test
public void smokeDetectorGone() throws IOException {
waitForAssert(() -> assertThat(bridge.getStatus(), is(ThingStatus.ONLINE)));
putStreamingEventData(fromFile(COMPLETE_DATA_FILE_NAME));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.ONLINE)));
putStreamingEventData(fromFile(EMPTY_DATA_FILE_NAME));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.OFFLINE)));
assertThat(thing.getStatusInfo().getStatusDetail(), is(ThingStatusDetail.GONE));
}
@Test
public void channelRefresh() throws IOException {
waitForAssert(() -> assertThat(bridge.getStatus(), is(ThingStatus.ONLINE)));
putStreamingEventData(fromFile(COMPLETE_DATA_FILE_NAME));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.ONLINE)));
assertThatAllItemStatesAreNotNull();
updateAllItemStatesToNull();
assertThatAllItemStatesAreNull();
refreshAllChannels();
assertThatAllItemStatesAreNotNull();
}
}

View File

@@ -0,0 +1,136 @@
/**
* 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.nest.handler;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.openhab.binding.nest.internal.NestBindingConstants.*;
import static org.openhab.binding.nest.internal.data.NestDataUtil.*;
import static org.openhab.core.library.types.OnOffType.OFF;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.openhab.binding.nest.internal.config.NestStructureConfiguration;
import org.openhab.binding.nest.internal.handler.NestStructureHandler;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.builder.ThingBuilder;
/**
* Tests for {@link NestStructureHandler}.
*
* @author Wouter Born - Increase test coverage
*/
public class NestStructureHandlerTest extends NestThingHandlerOSGiTest {
private static final ThingUID STRUCTURE_UID = new ThingUID(THING_TYPE_STRUCTURE, "structure1");
private static final int CHANNEL_COUNT = 11;
public NestStructureHandlerTest() {
super(NestStructureHandler.class);
}
@Override
protected Thing buildThing(Bridge bridge) {
Map<String, Object> properties = new HashMap<>();
properties.put(NestStructureConfiguration.STRUCTURE_ID, STRUCTURE1_STRUCTURE_ID);
return ThingBuilder.create(THING_TYPE_STRUCTURE, STRUCTURE_UID).withLabel("Test Structure")
.withBridge(bridge.getUID()).withChannels(buildChannels(THING_TYPE_STRUCTURE, STRUCTURE_UID))
.withConfiguration(new Configuration(properties)).build();
}
@Test
public void completeStructureUpdate() throws IOException {
assertThat(thing.getChannels().size(), is(CHANNEL_COUNT));
assertThat(thing.getStatus(), is(ThingStatus.OFFLINE));
waitForAssert(() -> assertThat(bridge.getStatus(), is(ThingStatus.ONLINE)));
putStreamingEventData(fromFile(COMPLETE_DATA_FILE_NAME));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.ONLINE)));
assertThatItemHasState(CHANNEL_AWAY, new StringType("HOME"));
assertThatItemHasState(CHANNEL_CO_ALARM_STATE, new StringType("OK"));
assertThatItemHasState(CHANNEL_COUNTRY_CODE, new StringType("US"));
assertThatItemHasState(CHANNEL_ETA_BEGIN, parseDateTimeType("2017-02-02T03:10:08.000Z"));
assertThatItemHasState(CHANNEL_PEAK_PERIOD_END_TIME, parseDateTimeType("2017-07-01T01:03:08.400Z"));
assertThatItemHasState(CHANNEL_PEAK_PERIOD_START_TIME, parseDateTimeType("2017-06-01T13:31:10.870Z"));
assertThatItemHasState(CHANNEL_POSTAL_CODE, new StringType("98056"));
assertThatItemHasState(CHANNEL_RUSH_HOUR_REWARDS_ENROLLMENT, OFF);
assertThatItemHasState(CHANNEL_SECURITY_STATE, new StringType("OK"));
assertThatItemHasState(CHANNEL_SMOKE_ALARM_STATE, new StringType("OK"));
assertThatItemHasState(CHANNEL_TIME_ZONE, new StringType("America/Los_Angeles"));
assertThatAllItemStatesAreNotNull();
}
@Test
public void incompleteStructureUpdate() throws IOException {
assertThat(thing.getChannels().size(), is(CHANNEL_COUNT));
assertThat(thing.getStatus(), is(ThingStatus.OFFLINE));
waitForAssert(() -> assertThat(bridge.getStatus(), is(ThingStatus.ONLINE)));
putStreamingEventData(fromFile(COMPLETE_DATA_FILE_NAME));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.ONLINE)));
assertThatAllItemStatesAreNotNull();
putStreamingEventData(fromFile(INCOMPLETE_DATA_FILE_NAME));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.ONLINE)));
assertThatAllItemStatesAreNull();
}
@Test
public void structureGone() throws IOException {
waitForAssert(() -> assertThat(bridge.getStatus(), is(ThingStatus.ONLINE)));
putStreamingEventData(fromFile(COMPLETE_DATA_FILE_NAME));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.ONLINE)));
putStreamingEventData(fromFile(EMPTY_DATA_FILE_NAME));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.OFFLINE)));
assertThat(thing.getStatusInfo().getStatusDetail(), is(ThingStatusDetail.GONE));
}
@Test
public void channelRefresh() throws IOException {
waitForAssert(() -> assertThat(bridge.getStatus(), is(ThingStatus.ONLINE)));
putStreamingEventData(fromFile(COMPLETE_DATA_FILE_NAME));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.ONLINE)));
assertThatAllItemStatesAreNotNull();
updateAllItemStatesToNull();
assertThatAllItemStatesAreNull();
refreshAllChannels();
assertThatAllItemStatesAreNotNull();
}
@Test
public void handleAwayCommands() throws IOException {
handleCommand(CHANNEL_AWAY, new StringType("AWAY"));
assertNestApiPropertyState(STRUCTURE1_STRUCTURE_ID, "away", "away");
handleCommand(CHANNEL_AWAY, new StringType("HOME"));
assertNestApiPropertyState(STRUCTURE1_STRUCTURE_ID, "away", "home");
handleCommand(CHANNEL_AWAY, new StringType("AWAY"));
assertNestApiPropertyState(STRUCTURE1_STRUCTURE_ID, "away", "away");
}
}

View File

@@ -0,0 +1,300 @@
/**
* 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.nest.handler;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.openhab.binding.nest.internal.NestBindingConstants.*;
import static org.openhab.binding.nest.internal.data.NestDataUtil.*;
import static org.openhab.core.library.types.OnOffType.*;
import static org.openhab.core.library.unit.ImperialUnits.FAHRENHEIT;
import static org.openhab.core.library.unit.SIUnits.CELSIUS;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.openhab.binding.nest.internal.config.NestDeviceConfiguration;
import org.openhab.binding.nest.internal.handler.NestThermostatHandler;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.builder.ThingBuilder;
/**
* Tests for {@link NestThermostatHandler}.
*
* @author Wouter Born - Increase test coverage
*/
public class NestThermostatHandlerTest extends NestThingHandlerOSGiTest {
private static final ThingUID THERMOSTAT_UID = new ThingUID(THING_TYPE_THERMOSTAT, "thermostat1");
private static final int CHANNEL_COUNT = 25;
public NestThermostatHandlerTest() {
super(NestThermostatHandler.class);
}
@Override
protected Thing buildThing(Bridge bridge) {
Map<String, Object> properties = new HashMap<>();
properties.put(NestDeviceConfiguration.DEVICE_ID, THERMOSTAT1_DEVICE_ID);
return ThingBuilder.create(THING_TYPE_THERMOSTAT, THERMOSTAT_UID).withLabel("Test Thermostat")
.withBridge(bridge.getUID()).withChannels(buildChannels(THING_TYPE_THERMOSTAT, THERMOSTAT_UID))
.withConfiguration(new Configuration(properties)).build();
}
@Test
public void completeThermostatCelsiusUpdate() throws IOException {
assertThat(thing.getChannels().size(), is(CHANNEL_COUNT));
assertThat(thing.getStatus(), is(ThingStatus.OFFLINE));
waitForAssert(() -> assertThat(bridge.getStatus(), is(ThingStatus.ONLINE)));
putStreamingEventData(fromFile(COMPLETE_DATA_FILE_NAME, CELSIUS));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.ONLINE)));
assertThatItemHasState(CHANNEL_CAN_COOL, OFF);
assertThatItemHasState(CHANNEL_CAN_HEAT, ON);
assertThatItemHasState(CHANNEL_ECO_MAX_SET_POINT, new QuantityType<>(24, CELSIUS));
assertThatItemHasState(CHANNEL_ECO_MIN_SET_POINT, new QuantityType<>(12.5, CELSIUS));
assertThatItemHasState(CHANNEL_FAN_TIMER_ACTIVE, OFF);
assertThatItemHasState(CHANNEL_FAN_TIMER_DURATION, new QuantityType<>(15, SmartHomeUnits.MINUTE));
assertThatItemHasState(CHANNEL_FAN_TIMER_TIMEOUT, parseDateTimeType("1970-01-01T00:00:00.000Z"));
assertThatItemHasState(CHANNEL_HAS_FAN, ON);
assertThatItemHasState(CHANNEL_HAS_LEAF, ON);
assertThatItemHasState(CHANNEL_HUMIDITY, new QuantityType<>(25, SmartHomeUnits.PERCENT));
assertThatItemHasState(CHANNEL_LAST_CONNECTION, parseDateTimeType("2017-02-02T21:00:06.000Z"));
assertThatItemHasState(CHANNEL_LOCKED, OFF);
assertThatItemHasState(CHANNEL_LOCKED_MAX_SET_POINT, new QuantityType<>(22, CELSIUS));
assertThatItemHasState(CHANNEL_LOCKED_MIN_SET_POINT, new QuantityType<>(20, CELSIUS));
assertThatItemHasState(CHANNEL_MAX_SET_POINT, new QuantityType<>(24, CELSIUS));
assertThatItemHasState(CHANNEL_MIN_SET_POINT, new QuantityType<>(20, CELSIUS));
assertThatItemHasState(CHANNEL_MODE, new StringType("HEAT"));
assertThatItemHasState(CHANNEL_PREVIOUS_MODE, new StringType("HEAT"));
assertThatItemHasState(CHANNEL_SET_POINT, new QuantityType<>(15.5, CELSIUS));
assertThatItemHasState(CHANNEL_STATE, new StringType("OFF"));
assertThatItemHasState(CHANNEL_SUNLIGHT_CORRECTION_ACTIVE, OFF);
assertThatItemHasState(CHANNEL_SUNLIGHT_CORRECTION_ENABLED, ON);
assertThatItemHasState(CHANNEL_TEMPERATURE, new QuantityType<>(19, CELSIUS));
assertThatItemHasState(CHANNEL_TIME_TO_TARGET, new QuantityType<>(0, SmartHomeUnits.MINUTE));
assertThatItemHasState(CHANNEL_USING_EMERGENCY_HEAT, OFF);
assertThatAllItemStatesAreNotNull();
}
@Test
public void completeThermostatFahrenheitUpdate() throws IOException {
assertThat(thing.getChannels().size(), is(CHANNEL_COUNT));
assertThat(thing.getStatus(), is(ThingStatus.OFFLINE));
waitForAssert(() -> assertThat(bridge.getStatus(), is(ThingStatus.ONLINE)));
putStreamingEventData(fromFile(COMPLETE_DATA_FILE_NAME, FAHRENHEIT));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.ONLINE)));
assertThatItemHasState(CHANNEL_CAN_COOL, OFF);
assertThatItemHasState(CHANNEL_CAN_HEAT, ON);
assertThatItemHasState(CHANNEL_ECO_MAX_SET_POINT, new QuantityType<>(76, FAHRENHEIT));
assertThatItemHasState(CHANNEL_ECO_MIN_SET_POINT, new QuantityType<>(55, FAHRENHEIT));
assertThatItemHasState(CHANNEL_FAN_TIMER_ACTIVE, OFF);
assertThatItemHasState(CHANNEL_FAN_TIMER_DURATION, new QuantityType<>(15, SmartHomeUnits.MINUTE));
assertThatItemHasState(CHANNEL_FAN_TIMER_TIMEOUT, parseDateTimeType("1970-01-01T00:00:00.000Z"));
assertThatItemHasState(CHANNEL_HAS_FAN, ON);
assertThatItemHasState(CHANNEL_HAS_LEAF, ON);
assertThatItemHasState(CHANNEL_HUMIDITY, new QuantityType<>(25, SmartHomeUnits.PERCENT));
assertThatItemHasState(CHANNEL_LAST_CONNECTION, parseDateTimeType("2017-02-02T21:00:06.000Z"));
assertThatItemHasState(CHANNEL_LOCKED, OFF);
assertThatItemHasState(CHANNEL_LOCKED_MAX_SET_POINT, new QuantityType<>(72, FAHRENHEIT));
assertThatItemHasState(CHANNEL_LOCKED_MIN_SET_POINT, new QuantityType<>(68, FAHRENHEIT));
assertThatItemHasState(CHANNEL_MAX_SET_POINT, new QuantityType<>(75, FAHRENHEIT));
assertThatItemHasState(CHANNEL_MIN_SET_POINT, new QuantityType<>(68, FAHRENHEIT));
assertThatItemHasState(CHANNEL_MODE, new StringType("HEAT"));
assertThatItemHasState(CHANNEL_PREVIOUS_MODE, new StringType("HEAT"));
assertThatItemHasState(CHANNEL_SET_POINT, new QuantityType<>(60, FAHRENHEIT));
assertThatItemHasState(CHANNEL_STATE, new StringType("OFF"));
assertThatItemHasState(CHANNEL_SUNLIGHT_CORRECTION_ACTIVE, OFF);
assertThatItemHasState(CHANNEL_SUNLIGHT_CORRECTION_ENABLED, ON);
assertThatItemHasState(CHANNEL_TEMPERATURE, new QuantityType<>(66, FAHRENHEIT));
assertThatItemHasState(CHANNEL_TIME_TO_TARGET, new QuantityType<>(0, SmartHomeUnits.MINUTE));
assertThatItemHasState(CHANNEL_USING_EMERGENCY_HEAT, OFF);
assertThatAllItemStatesAreNotNull();
}
@Test
public void incompleteThermostatUpdate() throws IOException {
assertThat(thing.getChannels().size(), is(CHANNEL_COUNT));
assertThat(thing.getStatus(), is(ThingStatus.OFFLINE));
waitForAssert(() -> assertThat(bridge.getStatus(), is(ThingStatus.ONLINE)));
putStreamingEventData(fromFile(COMPLETE_DATA_FILE_NAME));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.ONLINE)));
assertThatAllItemStatesAreNotNull();
putStreamingEventData(fromFile(INCOMPLETE_DATA_FILE_NAME));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.UNKNOWN)));
assertThatAllItemStatesAreNull();
}
@Test
public void thermostatGone() throws IOException {
waitForAssert(() -> assertThat(bridge.getStatus(), is(ThingStatus.ONLINE)));
putStreamingEventData(fromFile(COMPLETE_DATA_FILE_NAME));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.ONLINE)));
putStreamingEventData(fromFile(EMPTY_DATA_FILE_NAME));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.OFFLINE)));
assertThat(thing.getStatusInfo().getStatusDetail(), is(ThingStatusDetail.GONE));
}
@Test
public void channelRefresh() throws IOException {
waitForAssert(() -> assertThat(bridge.getStatus(), is(ThingStatus.ONLINE)));
putStreamingEventData(fromFile(COMPLETE_DATA_FILE_NAME));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.ONLINE)));
assertThatAllItemStatesAreNotNull();
updateAllItemStatesToNull();
assertThatAllItemStatesAreNull();
refreshAllChannels();
assertThatAllItemStatesAreNotNull();
}
@Test
public void handleFanTimerActiveCommands() throws IOException {
handleCommand(CHANNEL_FAN_TIMER_ACTIVE, ON);
assertNestApiPropertyState(THERMOSTAT1_DEVICE_ID, "fan_timer_active", "true");
handleCommand(CHANNEL_FAN_TIMER_ACTIVE, OFF);
assertNestApiPropertyState(THERMOSTAT1_DEVICE_ID, "fan_timer_active", "false");
handleCommand(CHANNEL_FAN_TIMER_ACTIVE, ON);
assertNestApiPropertyState(THERMOSTAT1_DEVICE_ID, "fan_timer_active", "true");
}
@Test
public void handleFanTimerDurationCommands() throws IOException {
int[] durations = { 15, 30, 45, 60, 120, 240, 480, 960, 15 };
for (int duration : durations) {
handleCommand(CHANNEL_FAN_TIMER_DURATION, new QuantityType<>(duration, SmartHomeUnits.MINUTE));
assertNestApiPropertyState(THERMOSTAT1_DEVICE_ID, "fan_timer_duration", String.valueOf(duration));
}
}
@Test
public void handleMaxSetPointCelsiusCommands() throws IOException {
celsiusCommandsTest(CHANNEL_MAX_SET_POINT, "target_temperature_high_c");
}
@Test
public void handleMaxSetPointFahrenheitCommands() throws IOException {
fahrenheitCommandsTest(CHANNEL_MAX_SET_POINT, "target_temperature_high_f");
}
@Test
public void handleMinSetPointCelsiusCommands() throws IOException {
celsiusCommandsTest(CHANNEL_MIN_SET_POINT, "target_temperature_low_c");
}
@Test
public void handleMinSetPointFahrenheitCommands() throws IOException {
fahrenheitCommandsTest(CHANNEL_MIN_SET_POINT, "target_temperature_low_f");
}
@Test
public void handleChannelModeCommands() throws IOException {
handleCommand(CHANNEL_MODE, new StringType("HEAT"));
assertNestApiPropertyState(THERMOSTAT1_DEVICE_ID, "hvac_mode", "heat");
handleCommand(CHANNEL_MODE, new StringType("COOL"));
assertNestApiPropertyState(THERMOSTAT1_DEVICE_ID, "hvac_mode", "cool");
handleCommand(CHANNEL_MODE, new StringType("HEAT_COOL"));
assertNestApiPropertyState(THERMOSTAT1_DEVICE_ID, "hvac_mode", "heat-cool");
handleCommand(CHANNEL_MODE, new StringType("ECO"));
assertNestApiPropertyState(THERMOSTAT1_DEVICE_ID, "hvac_mode", "eco");
handleCommand(CHANNEL_MODE, new StringType("OFF"));
assertNestApiPropertyState(THERMOSTAT1_DEVICE_ID, "hvac_mode", "off");
handleCommand(CHANNEL_MODE, new StringType("HEAT"));
assertNestApiPropertyState(THERMOSTAT1_DEVICE_ID, "hvac_mode", "heat");
}
@Test
public void handleSetPointCelsiusCommands() throws IOException {
celsiusCommandsTest(CHANNEL_SET_POINT, "target_temperature_c");
}
@Test
public void handleSetPointFahrenheitCommands() throws IOException {
fahrenheitCommandsTest(CHANNEL_SET_POINT, "target_temperature_f");
}
private void celsiusCommandsTest(String channelId, String apiPropertyName) throws IOException {
waitForAssert(() -> assertThat(bridge.getStatus(), is(ThingStatus.ONLINE)));
putStreamingEventData(fromFile(COMPLETE_DATA_FILE_NAME, CELSIUS));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.ONLINE)));
handleCommand(channelId, new QuantityType<>(20, CELSIUS));
assertNestApiPropertyState(THERMOSTAT1_DEVICE_ID, apiPropertyName, "20.0");
handleCommand(channelId, new QuantityType<>(21.123, CELSIUS));
assertNestApiPropertyState(THERMOSTAT1_DEVICE_ID, apiPropertyName, "21.0");
handleCommand(channelId, new QuantityType<>(22.541, CELSIUS));
assertNestApiPropertyState(THERMOSTAT1_DEVICE_ID, apiPropertyName, "22.5");
handleCommand(channelId, new QuantityType<>(23.74, CELSIUS));
assertNestApiPropertyState(THERMOSTAT1_DEVICE_ID, apiPropertyName, "23.5");
handleCommand(channelId, new QuantityType<>(23.75, CELSIUS));
assertNestApiPropertyState(THERMOSTAT1_DEVICE_ID, apiPropertyName, "24.0");
handleCommand(channelId, new QuantityType<>(70, FAHRENHEIT));
assertNestApiPropertyState(THERMOSTAT1_DEVICE_ID, apiPropertyName, "21.0");
}
private void fahrenheitCommandsTest(String channelId, String apiPropertyName) throws IOException {
waitForAssert(() -> assertThat(bridge.getStatus(), is(ThingStatus.ONLINE)));
putStreamingEventData(fromFile(COMPLETE_DATA_FILE_NAME, FAHRENHEIT));
waitForAssert(() -> assertThat(thing.getStatus(), is(ThingStatus.ONLINE)));
handleCommand(channelId, new QuantityType<>(70, FAHRENHEIT));
assertNestApiPropertyState(THERMOSTAT1_DEVICE_ID, apiPropertyName, "70");
handleCommand(channelId, new QuantityType<>(71.123, FAHRENHEIT));
assertNestApiPropertyState(THERMOSTAT1_DEVICE_ID, apiPropertyName, "71");
handleCommand(channelId, new QuantityType<>(71.541, FAHRENHEIT));
assertNestApiPropertyState(THERMOSTAT1_DEVICE_ID, apiPropertyName, "72");
handleCommand(channelId, new QuantityType<>(72.74, FAHRENHEIT));
assertNestApiPropertyState(THERMOSTAT1_DEVICE_ID, apiPropertyName, "73");
handleCommand(channelId, new QuantityType<>(73.75, FAHRENHEIT));
assertNestApiPropertyState(THERMOSTAT1_DEVICE_ID, apiPropertyName, "74");
handleCommand(channelId, new QuantityType<>(21, CELSIUS));
assertNestApiPropertyState(THERMOSTAT1_DEVICE_ID, apiPropertyName, "70");
}
}

View File

@@ -0,0 +1,361 @@
/**
* 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.nest.handler;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNot.not;
import static org.mockito.Mockito.*;
import static org.openhab.binding.nest.internal.rest.NestStreamingRestClient.PUT;
import java.io.IOException;
import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.function.Function;
import javax.ws.rs.client.ClientBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.servlet.ServletHolder;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.openhab.binding.nest.internal.config.NestBridgeConfiguration;
import org.openhab.binding.nest.internal.handler.NestBaseHandler;
import org.openhab.binding.nest.test.NestTestApiServlet;
import org.openhab.binding.nest.test.NestTestBridgeHandler;
import org.openhab.binding.nest.test.NestTestHandlerFactory;
import org.openhab.binding.nest.test.NestTestServer;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemFactory;
import org.openhab.core.items.ItemNotFoundException;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.items.events.ItemEventFactory;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.test.TestPortUtil;
import org.openhab.core.test.java.JavaOSGiTest;
import org.openhab.core.test.storage.VolatileStorageService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ManagedThingProvider;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingProvider;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.openhab.core.thing.binding.builder.BridgeBuilder;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.link.ItemChannelLink;
import org.openhab.core.thing.link.ManagedItemChannelLinkProvider;
import org.openhab.core.thing.type.ChannelDefinition;
import org.openhab.core.thing.type.ChannelGroupDefinition;
import org.openhab.core.thing.type.ChannelGroupType;
import org.openhab.core.thing.type.ChannelGroupTypeRegistry;
import org.openhab.core.thing.type.ChannelType;
import org.openhab.core.thing.type.ChannelTypeRegistry;
import org.openhab.core.thing.type.ThingType;
import org.openhab.core.thing.type.ThingTypeRegistry;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.jaxrs.client.SseEventSourceFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link NestThingHandlerOSGiTest} is an abstract base class for Nest OSGi based tests.
*
* @author Wouter Born - Increase test coverage
*/
public abstract class NestThingHandlerOSGiTest extends JavaOSGiTest {
private static final String SERVER_HOST = "127.0.0.1";
private static final int SERVER_PORT = TestPortUtil.findFreePort();
private static final int SERVER_TIMEOUT = -1;
private static final String REDIRECT_URL = "http://" + SERVER_HOST + ":" + SERVER_PORT;
private final Logger logger = LoggerFactory.getLogger(NestThingHandlerOSGiTest.class);
private static NestTestServer server;
private static NestTestApiServlet servlet = new NestTestApiServlet();
private ChannelTypeRegistry channelTypeRegistry;
private ChannelGroupTypeRegistry channelGroupTypeRegistry;
private ItemFactory itemFactory;
private ItemRegistry itemRegistry;
private EventPublisher eventPublisher;
private ManagedThingProvider managedThingProvider;
private ThingTypeRegistry thingTypeRegistry;
private ManagedItemChannelLinkProvider managedItemChannelLinkProvider;
private VolatileStorageService volatileStorageService = new VolatileStorageService();
protected Bridge bridge;
protected NestTestBridgeHandler bridgeHandler;
protected Thing thing;
protected NestBaseHandler<?> thingHandler;
private Class<? extends NestBaseHandler<?>> thingClass;
private NestTestHandlerFactory nestTestHandlerFactory;
private @NonNullByDefault({}) ClientBuilder clientBuilder;
private @NonNullByDefault({}) SseEventSourceFactory eventSourceFactory;
public NestThingHandlerOSGiTest(Class<? extends NestBaseHandler<?>> thingClass) {
this.thingClass = thingClass;
}
@BeforeAll
public static void setUpClass() throws Exception {
ServletHolder holder = new ServletHolder(servlet);
server = new NestTestServer(SERVER_HOST, SERVER_PORT, SERVER_TIMEOUT, holder);
server.startServer();
}
@BeforeEach
public void setUp() throws ItemNotFoundException {
registerService(volatileStorageService);
managedThingProvider = getService(ThingProvider.class, ManagedThingProvider.class);
assertThat("Could not get ManagedThingProvider", managedThingProvider, is(notNullValue()));
thingTypeRegistry = getService(ThingTypeRegistry.class);
assertThat("Could not get ThingTypeRegistry", thingTypeRegistry, is(notNullValue()));
channelTypeRegistry = getService(ChannelTypeRegistry.class);
assertThat("Could not get ChannelTypeRegistry", channelTypeRegistry, is(notNullValue()));
channelGroupTypeRegistry = getService(ChannelGroupTypeRegistry.class);
assertThat("Could not get ChannelGroupTypeRegistry", channelGroupTypeRegistry, is(notNullValue()));
eventPublisher = getService(EventPublisher.class);
assertThat("Could not get EventPublisher", eventPublisher, is(notNullValue()));
itemFactory = getService(ItemFactory.class);
assertThat("Could not get ItemFactory", itemFactory, is(notNullValue()));
itemRegistry = getService(ItemRegistry.class);
assertThat("Could not get ItemRegistry", itemRegistry, is(notNullValue()));
managedItemChannelLinkProvider = getService(ManagedItemChannelLinkProvider.class);
assertThat("Could not get ManagedItemChannelLinkProvider", managedItemChannelLinkProvider, is(notNullValue()));
clientBuilder = getService(ClientBuilder.class);
assertThat("Could not get ClientBuilder", clientBuilder, is(notNullValue()));
eventSourceFactory = getService(SseEventSourceFactory.class);
assertThat("Could not get SseEventSourceFactory", eventSourceFactory, is(notNullValue()));
ComponentContext componentContext = mock(ComponentContext.class);
when(componentContext.getBundleContext()).thenReturn(bundleContext);
nestTestHandlerFactory = new NestTestHandlerFactory(clientBuilder, eventSourceFactory);
nestTestHandlerFactory.activate(componentContext,
Map.of(NestTestHandlerFactory.REDIRECT_URL_CONFIG_PROPERTY, REDIRECT_URL));
registerService(nestTestHandlerFactory);
nestTestHandlerFactory = getService(ThingHandlerFactory.class, NestTestHandlerFactory.class);
assertThat("Could not get NestTestHandlerFactory", nestTestHandlerFactory, is(notNullValue()));
bridge = buildBridge();
thing = buildThing(bridge);
bridgeHandler = addThing(bridge, NestTestBridgeHandler.class);
thingHandler = addThing(thing, thingClass);
createAndLinkItems();
assertThatAllItemStatesAreNull();
}
@AfterEach
public void tearDown() {
servlet.reset();
servlet.closeConnections();
if (thing != null) {
managedThingProvider.remove(thing.getUID());
}
if (bridge != null) {
managedThingProvider.remove(bridge.getUID());
}
unregisterService(volatileStorageService);
}
protected Bridge buildBridge() {
Map<String, Object> properties = new HashMap<>();
properties.put(NestBridgeConfiguration.ACCESS_TOKEN,
"c.eQ5QBBPiFOTNzPHbmZPcE9yPZ7GayzLusifgQR2DQRFNyUS9ESvlhJF0D7vG8Y0TFV39zX1vIOsWrv8RKCMrFepNUb9FqHEboa4MtWLUsGb4tD9oBh0jrV4HooJUmz5sVA5KZR0dkxyLYyPc");
properties.put(NestBridgeConfiguration.PINCODE, "64P2XRYT");
properties.put(NestBridgeConfiguration.PRODUCT_ID, "8fdf9885-ca07-4252-1aa3-f3d5ca9589e0");
properties.put(NestBridgeConfiguration.PRODUCT_SECRET, "QITLR3iyUlWaj9dbvCxsCKp4f");
return BridgeBuilder.create(NestTestBridgeHandler.THING_TYPE_TEST_BRIDGE, "test_account")
.withLabel("Test Account").withConfiguration(new Configuration(properties)).build();
}
protected abstract Thing buildThing(Bridge bridge);
protected List<Channel> buildChannels(ThingTypeUID thingTypeUID, ThingUID thingUID) {
waitForAssert(() -> assertThat(thingTypeRegistry.getThingType(thingTypeUID), notNullValue()));
ThingType thingType = thingTypeRegistry.getThingType(thingTypeUID);
List<Channel> channels = new ArrayList<>();
channels.addAll(buildChannels(thingUID, thingType.getChannelDefinitions(), (id) -> id));
for (ChannelGroupDefinition channelGroupDefinition : thingType.getChannelGroupDefinitions()) {
ChannelGroupType channelGroupType = channelGroupTypeRegistry
.getChannelGroupType(channelGroupDefinition.getTypeUID());
String groupId = channelGroupDefinition.getId();
if (channelGroupType != null) {
channels.addAll(
buildChannels(thingUID, channelGroupType.getChannelDefinitions(), (id) -> groupId + "#" + id));
}
}
channels.sort((Channel c1, Channel c2) -> c1.getUID().getId().compareTo(c2.getUID().getId()));
return channels;
}
protected List<Channel> buildChannels(ThingUID thingUID, List<ChannelDefinition> channelDefinitions,
Function<String, String> channelIdFunction) {
List<Channel> result = new ArrayList<>();
for (ChannelDefinition channelDefinition : channelDefinitions) {
ChannelType channelType = channelTypeRegistry.getChannelType(channelDefinition.getChannelTypeUID());
if (channelType != null) {
result.add(ChannelBuilder
.create(new ChannelUID(thingUID, channelIdFunction.apply(channelDefinition.getId())),
channelType.getItemType())
.build());
}
}
return result;
}
@SuppressWarnings("unchecked")
protected <T> T addThing(Thing thing, Class<T> thingHandlerClass) {
assertThat(thing.getHandler(), is(nullValue()));
managedThingProvider.add(thing);
waitForAssert(() -> assertThat(thing.getHandler(), notNullValue()));
assertThat(thing.getConfiguration(), is(notNullValue()));
assertThat(thing.getHandler(), is(instanceOf(thingHandlerClass)));
return (T) thing.getHandler();
}
protected String getThingId() {
return thing.getUID().getId();
}
protected ThingUID getThingUID() {
return thing.getUID();
}
protected void putStreamingEventData(String json) throws IOException {
String singleLineJson = json.replaceAll("\n\r\\s+", "").replaceAll("\n\\s+", "").replaceAll("\n\r", "")
.replaceAll("\n", "");
servlet.queueEvent(PUT, singleLineJson);
}
protected void createAndLinkItems() {
thing.getChannels().forEach(c -> {
String itemName = getItemName(c.getUID().getId());
Item item = itemFactory.createItem(c.getAcceptedItemType(), itemName);
if (item != null) {
itemRegistry.add(item);
}
managedItemChannelLinkProvider.add(new ItemChannelLink(itemName, c.getUID()));
});
}
protected void assertThatItemHasState(String channelId, State state) {
waitForAssert(() -> assertThat("Wrong state for item of channel '" + channelId + "' ", getItemState(channelId),
is(state)));
}
protected void assertThatItemHasNotState(String channelId, State state) {
waitForAssert(() -> assertThat("Wrong state for item of channel '" + channelId + "' ", getItemState(channelId),
is(not(state))));
}
protected void assertThatAllItemStatesAreNull() {
thing.getChannels().forEach(c -> assertThatItemHasState(c.getUID().getId(), UnDefType.NULL));
}
protected void assertThatAllItemStatesAreNotNull() {
thing.getChannels().forEach(c -> assertThatItemHasNotState(c.getUID().getId(), UnDefType.NULL));
}
protected ChannelUID getChannelUID(String channelId) {
return new ChannelUID(getThingUID(), channelId);
}
protected String getItemName(String channelId) {
return getThingId() + "_" + channelId.replaceAll("#", "_");
}
private State getItemState(String channelId) {
String itemName = getItemName(channelId);
try {
return itemRegistry.getItem(itemName).getState();
} catch (ItemNotFoundException e) {
throw new AssertionError("Item with name '" + itemName + "' not found");
}
}
protected void logItemStates() {
thing.getChannels().forEach(c -> {
String channelId = c.getUID().getId();
String itemName = getItemName(channelId);
logger.debug("{} = {}", itemName, getItemState(channelId));
});
}
protected void updateAllItemStatesToNull() {
thing.getChannels().forEach(c -> updateItemState(c.getUID().getId(), UnDefType.NULL));
}
protected void refreshAllChannels() {
thing.getChannels().forEach(c -> thingHandler.handleCommand(c.getUID(), RefreshType.REFRESH));
}
protected void handleCommand(String channelId, Command command) {
thingHandler.handleCommand(getChannelUID(channelId), command);
}
protected void updateItemState(String channelId, State state) {
String itemName = getItemName(channelId);
eventPublisher.post(ItemEventFactory.createStateEvent(itemName, state));
}
protected void assertNestApiPropertyState(String nestId, String propertyName, String state) {
waitForAssert(() -> assertThat(servlet.getNestIdPropertyState(nestId, propertyName), is(state)));
}
public static DateTimeType parseDateTimeType(String text) {
try {
return new DateTimeType(Instant.parse(text).atZone(TimeZone.getDefault().toZoneId()));
} catch (DateTimeParseException e) {
throw new IllegalArgumentException("Invalid date time argument: " + text, e);
}
}
}

View File

@@ -0,0 +1,223 @@
/**
* 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.nest.internal.data;
import static org.junit.jupiter.api.Assertions.*;
import static org.openhab.binding.nest.internal.data.NestDataUtil.*;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.junit.jupiter.api.Test;
import org.openhab.core.library.unit.SIUnits;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Test cases for gson parsing of model classes
*
* @author David Bennett - Initial contribution
* @author Wouter Born - Increase test coverage
*/
public class GsonParsingTest {
private final Logger logger = LoggerFactory.getLogger(GsonParsingTest.class);
private static void assertEqualDateTime(String expected, Date actual) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
assertEquals(expected, sdf.format(actual));
}
@Test
public void verifyCompleteInput() throws IOException {
TopLevelData topLevel = fromJson("top-level-data.json", TopLevelData.class);
assertEquals(topLevel.getDevices().getThermostats().size(), 1);
assertNotNull(topLevel.getDevices().getThermostats().get(THERMOSTAT1_DEVICE_ID));
assertEquals(topLevel.getDevices().getCameras().size(), 2);
assertNotNull(topLevel.getDevices().getCameras().get(CAMERA1_DEVICE_ID));
assertNotNull(topLevel.getDevices().getCameras().get(CAMERA2_DEVICE_ID));
assertEquals(topLevel.getDevices().getSmokeCoAlarms().size(), 4);
assertNotNull(topLevel.getDevices().getSmokeCoAlarms().get(SMOKE1_DEVICE_ID));
assertNotNull(topLevel.getDevices().getSmokeCoAlarms().get(SMOKE2_DEVICE_ID));
assertNotNull(topLevel.getDevices().getSmokeCoAlarms().get(SMOKE3_DEVICE_ID));
assertNotNull(topLevel.getDevices().getSmokeCoAlarms().get(SMOKE4_DEVICE_ID));
}
@Test
public void verifyCompleteStreamingInput() throws IOException {
TopLevelStreamingData topLevelStreamingData = fromJson("top-level-streaming-data.json",
TopLevelStreamingData.class);
assertEquals("/", topLevelStreamingData.getPath());
TopLevelData data = topLevelStreamingData.getData();
assertEquals(data.getDevices().getThermostats().size(), 1);
assertNotNull(data.getDevices().getThermostats().get(THERMOSTAT1_DEVICE_ID));
assertEquals(data.getDevices().getCameras().size(), 2);
assertNotNull(data.getDevices().getCameras().get(CAMERA1_DEVICE_ID));
assertNotNull(data.getDevices().getCameras().get(CAMERA2_DEVICE_ID));
assertEquals(data.getDevices().getSmokeCoAlarms().size(), 4);
assertNotNull(data.getDevices().getSmokeCoAlarms().get(SMOKE1_DEVICE_ID));
assertNotNull(data.getDevices().getSmokeCoAlarms().get(SMOKE2_DEVICE_ID));
assertNotNull(data.getDevices().getSmokeCoAlarms().get(SMOKE3_DEVICE_ID));
assertNotNull(data.getDevices().getSmokeCoAlarms().get(SMOKE4_DEVICE_ID));
}
@Test
public void verifyThermostat() throws IOException {
Thermostat thermostat = fromJson("thermostat-data.json", Thermostat.class);
logger.debug("Thermostat: {}", thermostat);
assertTrue(thermostat.isOnline());
assertTrue(thermostat.isCanHeat());
assertTrue(thermostat.isHasLeaf());
assertFalse(thermostat.isCanCool());
assertFalse(thermostat.isFanTimerActive());
assertFalse(thermostat.isLocked());
assertFalse(thermostat.isSunlightCorrectionActive());
assertTrue(thermostat.isSunlightCorrectionEnabled());
assertFalse(thermostat.isUsingEmergencyHeat());
assertEquals(THERMOSTAT1_DEVICE_ID, thermostat.getDeviceId());
assertEquals(Integer.valueOf(15), thermostat.getFanTimerDuration());
assertEqualDateTime("2017-02-02T21:00:06.000Z", thermostat.getLastConnection());
assertEqualDateTime("1970-01-01T00:00:00.000Z", thermostat.getFanTimerTimeout());
assertEquals(Double.valueOf(24.0), thermostat.getEcoTemperatureHigh());
assertEquals(Double.valueOf(12.5), thermostat.getEcoTemperatureLow());
assertEquals(Double.valueOf(22.0), thermostat.getLockedTempMax());
assertEquals(Double.valueOf(20.0), thermostat.getLockedTempMin());
assertEquals(Thermostat.Mode.HEAT, thermostat.getMode());
assertEquals("Living Room (Living Room)", thermostat.getName());
assertEquals("Living Room Thermostat (Living Room)", thermostat.getNameLong());
assertEquals(null, thermostat.getPreviousHvacMode());
assertEquals("5.6-7", thermostat.getSoftwareVersion());
assertEquals(Thermostat.State.OFF, thermostat.getHvacState());
assertEquals(STRUCTURE1_STRUCTURE_ID, thermostat.getStructureId());
assertEquals(Double.valueOf(15.5), thermostat.getTargetTemperature());
assertEquals(Double.valueOf(24.0), thermostat.getTargetTemperatureHigh());
assertEquals(Double.valueOf(20.0), thermostat.getTargetTemperatureLow());
assertEquals(SIUnits.CELSIUS, thermostat.getTemperatureUnit());
assertEquals(Integer.valueOf(0), thermostat.getTimeToTarget());
assertEquals(THERMOSTAT1_WHERE_ID, thermostat.getWhereId());
assertEquals("Living Room", thermostat.getWhereName());
}
@Test
public void thermostatTimeToTargetSupportedValueParsing() {
assertEquals((Integer) 0, Thermostat.parseTimeToTarget("~0"));
assertEquals((Integer) 5, Thermostat.parseTimeToTarget("<5"));
assertEquals((Integer) 10, Thermostat.parseTimeToTarget("<10"));
assertEquals((Integer) 15, Thermostat.parseTimeToTarget("~15"));
assertEquals((Integer) 90, Thermostat.parseTimeToTarget("~90"));
assertEquals((Integer) 120, Thermostat.parseTimeToTarget(">120"));
}
@Test
public void thermostatTimeToTargetUnsupportedValueParsing() {
assertThrows(NumberFormatException.class, () -> Thermostat.parseTimeToTarget("#5"));
}
@Test
public void verifyCamera() throws IOException {
Camera camera = fromJson("camera-data.json", Camera.class);
logger.debug("Camera: {}", camera);
assertTrue(camera.isOnline());
assertEquals("Upstairs", camera.getName());
assertEquals("Upstairs Camera", camera.getNameLong());
assertEquals(STRUCTURE1_STRUCTURE_ID, camera.getStructureId());
assertEquals(CAMERA1_WHERE_ID, camera.getWhereId());
assertTrue(camera.isAudioInputEnabled());
assertFalse(camera.isPublicShareEnabled());
assertFalse(camera.isStreaming());
assertFalse(camera.isVideoHistoryEnabled());
assertEquals("https://camera_app_url", camera.getAppUrl());
assertEquals(CAMERA1_DEVICE_ID, camera.getDeviceId());
assertNull(camera.getLastConnection());
assertEqualDateTime("2017-01-22T08:19:20.000Z", camera.getLastIsOnlineChange());
assertNull(camera.getPublicShareUrl());
assertEquals("https://camera_snapshot_url", camera.getSnapshotUrl());
assertEquals("205-600052", camera.getSoftwareVersion());
assertEquals("https://camera_web_url", camera.getWebUrl());
assertEquals("https://last_event_animated_image_url", camera.getLastEvent().getAnimatedImageUrl());
assertEquals(2, camera.getLastEvent().getActivityZones().size());
assertEquals("id1", camera.getLastEvent().getActivityZones().get(0));
assertEquals("https://last_event_app_url", camera.getLastEvent().getAppUrl());
assertEqualDateTime("2017-01-22T07:40:38.680Z", camera.getLastEvent().getEndTime());
assertEquals("https://last_event_image_url", camera.getLastEvent().getImageUrl());
assertEqualDateTime("2017-01-22T07:40:19.020Z", camera.getLastEvent().getStartTime());
assertEqualDateTime("2017-02-05T07:40:19.020Z", camera.getLastEvent().getUrlsExpireTime());
assertEquals("https://last_event_web_url", camera.getLastEvent().getWebUrl());
assertTrue(camera.getLastEvent().isHasMotion());
assertFalse(camera.getLastEvent().isHasPerson());
assertFalse(camera.getLastEvent().isHasSound());
}
@Test
public void verifySmokeDetector() throws IOException {
SmokeDetector smokeDetector = fromJson("smoke-detector-data.json", SmokeDetector.class);
logger.debug("SmokeDetector: {}", smokeDetector);
assertTrue(smokeDetector.isOnline());
assertEquals(SMOKE1_WHERE_ID, smokeDetector.getWhereId());
assertEquals(SMOKE1_DEVICE_ID, smokeDetector.getDeviceId());
assertEquals("Downstairs", smokeDetector.getName());
assertEquals("Downstairs Nest Protect", smokeDetector.getNameLong());
assertEqualDateTime("2017-02-02T20:53:05.338Z", smokeDetector.getLastConnection());
assertEquals(SmokeDetector.BatteryHealth.OK, smokeDetector.getBatteryHealth());
assertEquals(SmokeDetector.AlarmState.OK, smokeDetector.getCoAlarmState());
assertEquals(SmokeDetector.AlarmState.OK, smokeDetector.getSmokeAlarmState());
assertEquals("3.1rc9", smokeDetector.getSoftwareVersion());
assertEquals(STRUCTURE1_STRUCTURE_ID, smokeDetector.getStructureId());
assertEquals(SmokeDetector.UiColorState.GREEN, smokeDetector.getUiColorState());
}
@Test
public void verifyAccessToken() throws IOException {
AccessTokenData accessToken = fromJson("access-token-data.json", AccessTokenData.class);
logger.debug("AccessTokenData: {}", accessToken);
assertEquals("access_token", accessToken.getAccessToken());
assertEquals(Long.valueOf(315360000L), accessToken.getExpiresIn());
}
@Test
public void verifyStructure() throws IOException {
Structure structure = fromJson("structure-data.json", Structure.class);
logger.debug("Structure: {}", structure);
assertEquals("Home", structure.getName());
assertEquals("US", structure.getCountryCode());
assertEquals("98056", structure.getPostalCode());
assertEquals(Structure.HomeAwayState.HOME, structure.getAway());
assertEqualDateTime("2017-02-02T03:10:08.000Z", structure.getEtaBegin());
assertNull(structure.getEta());
assertNull(structure.getPeakPeriodEndTime());
assertNull(structure.getPeakPeriodStartTime());
assertEquals(STRUCTURE1_STRUCTURE_ID, structure.getStructureId());
assertEquals("America/Los_Angeles", structure.getTimeZone());
assertFalse(structure.isRhrEnrollment());
}
@Test
public void verifyError() throws IOException {
ErrorData error = fromJson("error-data.json", ErrorData.class);
logger.debug("ErrorData: {}", error);
assertEquals("blocked", error.getError());
assertEquals("https://developer.nest.com/documentation/cloud/error-messages#blocked", error.getType());
assertEquals("blocked", error.getMessage());
assertEquals("bb514046-edc9-4bca-8239-f7a3cfb0925a", error.getInstance());
}
}

View File

@@ -0,0 +1,96 @@
/**
* 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.nest.internal.data;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.stream.Collectors;
import javax.measure.Unit;
import javax.measure.quantity.Temperature;
import org.openhab.binding.nest.internal.NestUtils;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.library.unit.SIUnits;
/**
* Utility class for working with Nest test data in unit tests.
*
* @author Wouter Born - Increase test coverage
*/
public final class NestDataUtil {
public static final String COMPLETE_DATA_FILE_NAME = "top-level-streaming-data.json";
public static final String INCOMPLETE_DATA_FILE_NAME = "top-level-streaming-data-incomplete.json";
public static final String EMPTY_DATA_FILE_NAME = "top-level-streaming-data-empty.json";
public static final String CAMERA1_DEVICE_ID = "_LK8j9rRXwCKEBOtDo7JskNxzWfHBOIm3CLouCT3FQZzrvokK_DzFQ";
public static final String CAMERA1_WHERE_ID = "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKCxvyZfxNpKA";
public static final String CAMERA2_DEVICE_ID = "VG7C7BU6Zf8OjEfizmBCVnwnuKHSnOBIHgbQKa57xKJzrvokK_DzFQ";
public static final String CAMERA2_WHERE_ID = "qpWvTu89Knhn6GRFM-VtGoE4KYwbzbJg9INR6WyPfhW1EJ04GRyYbQ";
public static final String SMOKE1_DEVICE_ID = "p1b1oySOcs_sbi4iczruW3Ou-iQr8PMV";
public static final String SMOKE1_WHERE_ID = "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIm5E0NfJPeeg";
public static final String SMOKE2_DEVICE_ID = "p1b1oySOcs8W9WwaNu80oXOu-iQr8PMV";
public static final String SMOKE2_WHERE_ID = "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKCxvyZfxNpKA";
public static final String SMOKE3_DEVICE_ID = "p1b1oySOcs-OJHIgmgeMkHOu-iQr8PMV";
public static final String SMOKE3_WHERE_ID = "6UAWzz8czKpFrH6EK3AcjDiTjbRgts8x5MJxEnn1yKKQpYTBO7n2UQ";
public static final String SMOKE4_DEVICE_ID = "p1b1oySOcs8Qu7IAJVrQ7XOu-iQr8PMV";
public static final String SMOKE4_WHERE_ID = "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKQrCrjN0yXiw";
public static final String STRUCTURE1_STRUCTURE_ID = "ysCnsCaq1pQwKUPP9H4AqE943C1XtLin3x6uCVN5Qh09IDyTg7Ey5A";
public static final String THERMOSTAT1_DEVICE_ID = "G1jouHN5yl6mXFaQw5iGwXOu-iQr8PMV";
public static final String THERMOSTAT1_WHERE_ID = "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKQrCrjN0yXiw";
private NestDataUtil() {
// Hidden utility class constructor
}
public static Reader openDataReader(String fileName) throws UnsupportedEncodingException {
String packagePath = (NestDataUtil.class.getPackage().getName()).replaceAll("\\.", "/");
String filePath = "/" + packagePath + "/" + fileName;
InputStream inputStream = NestDataUtil.class.getClassLoader().getResourceAsStream(filePath);
return new InputStreamReader(inputStream, "UTF-8");
}
public static <T> T fromJson(String fileName, Class<T> dataClass) throws IOException {
try (Reader reader = openDataReader(fileName)) {
return NestUtils.fromJson(reader, dataClass);
}
}
public static String fromFile(String fileName, Unit<Temperature> temperatureUnit) throws IOException {
String json = fromFile(fileName);
if (temperatureUnit == SIUnits.CELSIUS) {
json = json.replace("\"temperature_scale\": \"F\"", "\"temperature_scale\": \"C\"");
} else if (temperatureUnit == ImperialUnits.FAHRENHEIT) {
json = json.replace("\"temperature_scale\": \"C\"", "\"temperature_scale\": \"F\"");
}
return json;
}
public static String fromFile(String fileName) throws IOException {
try (Reader reader = openDataReader(fileName)) {
return new BufferedReader(reader).lines().parallel().collect(Collectors.joining("\n"));
}
}
}

View File

@@ -0,0 +1,220 @@
/**
* 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.nest.test;
import static org.openhab.binding.nest.internal.NestBindingConstants.*;
import static org.openhab.binding.nest.internal.rest.NestStreamingRestClient.*;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
/**
* The {@link NestTestApiServlet} mocks the Nest API during tests.
*
* @author Wouter Born - Increase test coverage
*/
public class NestTestApiServlet extends HttpServlet {
private static final long serialVersionUID = -5414910055159062745L;
private static final String NEW_LINE = "\n";
private static final String UPDATE_PATHS[] = { NEST_CAMERA_UPDATE_PATH, NEST_SMOKE_ALARM_UPDATE_PATH,
NEST_STRUCTURE_UPDATE_PATH, NEST_THERMOSTAT_UPDATE_PATH };
private final Logger logger = LoggerFactory.getLogger(NestTestApiServlet.class);
private class SseEvent {
private String name;
private String data;
public SseEvent(String name) {
this.name = name;
}
public SseEvent(String name, String data) {
this.name = name;
this.data = data;
}
public String getData() {
return data;
}
public String getName() {
return name;
}
public boolean hasData() {
return data != null && !data.isEmpty();
}
}
private final Map<String, Map<String, String>> nestIdPropertiesMap = new ConcurrentHashMap<>();
private final Map<Thread, Queue<SseEvent>> listenerQueues = new ConcurrentHashMap<>();
private final ThreadLocal<PrintWriter> threadLocalWriter = new ThreadLocal<>();
private final Gson gson = new GsonBuilder().create();
public void closeConnections() {
Set<Thread> threads = listenerQueues.keySet();
listenerQueues.clear();
threads.forEach(thread -> thread.interrupt());
}
public void reset() {
nestIdPropertiesMap.clear();
}
public void queueEvent(String eventName) {
SseEvent event = new SseEvent(eventName);
listenerQueues.forEach((thread, queue) -> queue.add(event));
}
public void queueEvent(String eventName, String data) {
SseEvent event = new SseEvent(eventName, data);
listenerQueues.forEach((thread, queue) -> queue.add(event));
}
@SuppressWarnings("resource")
private void writeEvent(SseEvent event) {
logger.debug("Writing {} event", event.getName());
PrintWriter writer = threadLocalWriter.get();
writer.write("event: ");
writer.write(event.getName());
writer.write(NEW_LINE);
if (event.hasData()) {
for (String dataLine : event.getData().split(NEW_LINE)) {
writer.write("data: ");
writer.write(dataLine);
writer.write(NEW_LINE);
}
}
writer.write(NEW_LINE);
writer.flush();
}
private void writeEvent(String eventName) {
writeEvent(new SseEvent(eventName));
}
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
ArrayBlockingQueue<SseEvent> queue = new ArrayBlockingQueue<>(10);
listenerQueues.put(Thread.currentThread(), queue);
response.setContentType("text/event-stream");
response.setCharacterEncoding("UTF-8");
response.flushBuffer();
logger.debug("Opened event stream to {}:{}", request.getRemoteHost(), request.getRemotePort());
PrintWriter writer = response.getWriter();
threadLocalWriter.set(writer);
writeEvent(OPEN);
while (listenerQueues.containsKey(Thread.currentThread()) && !writer.checkError()) {
try {
SseEvent event = queue.poll(KEEP_ALIVE_MILLIS, TimeUnit.MILLISECONDS);
if (event != null) {
writeEvent(event);
} else {
writeEvent(KEEP_ALIVE);
}
} catch (InterruptedException e) {
logger.debug("Evaluating loop conditions after interrupt");
}
}
listenerQueues.remove(Thread.currentThread());
threadLocalWriter.remove();
writer.close();
logger.debug("Closed event stream to {}:{}", request.getRemoteHost(), request.getRemotePort());
}
@Override
protected void doPut(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
logger.debug("Received put request: {}", request);
String uri = request.getRequestURI();
String nestId = getNestIdFromURI(uri);
if (nestId == null) {
logger.error("Unsupported URI: {}", uri);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return;
}
InputStreamReader reader = new InputStreamReader(request.getInputStream());
Map<String, String> propertiesUpdate = gson.fromJson(reader, new TypeToken<Map<String, String>>() {
}.getType());
Map<String, String> properties = getOrCreateProperties(nestId);
properties.putAll(propertiesUpdate);
gson.toJson(propertiesUpdate, response.getWriter());
response.setStatus(HttpServletResponse.SC_OK);
}
private String getNestIdFromURI(String uri) {
for (String updatePath : UPDATE_PATHS) {
if (uri.startsWith(updatePath)) {
return uri.replaceAll(updatePath, "");
}
}
return null;
}
private Map<String, String> getOrCreateProperties(String nestId) {
Map<String, String> properties = nestIdPropertiesMap.get(nestId);
if (properties == null) {
properties = new HashMap<>();
nestIdPropertiesMap.put(nestId, properties);
}
return properties;
}
public String getNestIdPropertyState(String nestId, String propertyName) {
Map<String, String> properties = nestIdPropertiesMap.get(nestId);
return properties == null ? null : properties.get(propertyName);
}
}

View File

@@ -0,0 +1,66 @@
/**
* 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.nest.test;
import static org.openhab.binding.nest.internal.NestBindingConstants.BINDING_ID;
import java.util.Collections;
import java.util.Properties;
import java.util.Set;
import javax.ws.rs.client.ClientBuilder;
import org.openhab.binding.nest.internal.exceptions.InvalidAccessTokenException;
import org.openhab.binding.nest.internal.handler.NestBridgeHandler;
import org.openhab.binding.nest.internal.handler.NestRedirectUrlSupplier;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingTypeUID;
import org.osgi.service.jaxrs.client.SseEventSourceFactory;
/**
* The {@link NestTestBridgeHandler} is a {@link NestBridgeHandler} modified for testing. Using the
* {@link NestTestRedirectUrlSupplier} it will always connect to same provided {@link #redirectUrl}.
*
* @author Wouter Born - Increase test coverage
*/
public class NestTestBridgeHandler extends NestBridgeHandler {
class NestTestRedirectUrlSupplier extends NestRedirectUrlSupplier {
NestTestRedirectUrlSupplier(Properties httpHeaders) {
super(httpHeaders);
this.cachedUrl = redirectUrl;
}
@Override
public void resetCache() {
// Skip resetting the URL so the test server keeps being used
}
}
public static final ThingTypeUID THING_TYPE_TEST_BRIDGE = new ThingTypeUID(BINDING_ID, "test_account");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_TEST_BRIDGE);
private String redirectUrl;
public NestTestBridgeHandler(Bridge bridge, ClientBuilder clientBuilder, SseEventSourceFactory eventSourceFactory,
String redirectUrl) {
super(bridge, clientBuilder, eventSourceFactory);
this.redirectUrl = redirectUrl;
}
@Override
protected NestRedirectUrlSupplier createRedirectUrlSupplier() throws InvalidAccessTokenException {
return new NestTestRedirectUrlSupplier(getHttpHeaders());
}
}

View File

@@ -0,0 +1,117 @@
/**
* 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.nest.test;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import javax.ws.rs.client.ClientBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nest.internal.discovery.NestDiscoveryService;
import org.openhab.binding.nest.internal.handler.NestBridgeHandler;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.jaxrs.client.SseEventSourceFactory;
/**
* The {@link NestTestHandlerFactory} is responsible for creating test things and thing handlers.
*
* @author Wouter Born - Increase test coverage
*/
@NonNullByDefault
public class NestTestHandlerFactory extends BaseThingHandlerFactory implements ThingHandlerFactory {
public static final String REDIRECT_URL_CONFIG_PROPERTY = "redirect.url";
private final ClientBuilder clientBuilder;
private final SseEventSourceFactory eventSourceFactory;
private final Map<ThingUID, ServiceRegistration<?>> discoveryService = new HashMap<>();
private String redirectUrl = "http://localhost";
@Activate
public NestTestHandlerFactory(@Reference ClientBuilder clientBuilder,
@Reference SseEventSourceFactory eventSourceFactory) {
this.clientBuilder = clientBuilder;
this.eventSourceFactory = eventSourceFactory;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return NestTestBridgeHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID);
}
@Activate
public void activate(ComponentContext componentContext, Map<String, Object> config) {
super.activate(componentContext);
modified(config);
}
@Modified
public void modified(Map<String, Object> config) {
String url = (String) config.get(REDIRECT_URL_CONFIG_PROPERTY);
if (url != null) {
this.redirectUrl = url;
}
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(NestTestBridgeHandler.THING_TYPE_TEST_BRIDGE)) {
NestTestBridgeHandler handler = new NestTestBridgeHandler((Bridge) thing, clientBuilder, eventSourceFactory,
redirectUrl);
NestDiscoveryService service = new NestDiscoveryService(handler);
// Register the discovery service.
discoveryService.put(handler.getThing().getUID(),
bundleContext.registerService(DiscoveryService.class.getName(), service, new Hashtable<>()));
return handler;
}
return null;
}
/**
* Removes the handler for the specific thing. This also handles disabling the discovery
* service when the bridge is removed.
*/
@Override
protected void removeHandler(ThingHandler thingHandler) {
if (thingHandler instanceof NestBridgeHandler) {
ServiceRegistration<?> registration = discoveryService.get(thingHandler.getThing().getUID());
if (registration != null) {
// Unregister the discovery service.
NestDiscoveryService service = (NestDiscoveryService) bundleContext
.getService(registration.getReference());
service.deactivate();
registration.unregister();
discoveryService.remove(thingHandler.getThing().getUID());
}
}
super.removeHandler(thingHandler);
}
}

View File

@@ -0,0 +1,88 @@
/**
* 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.nest.test;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Embedded jetty server used in the tests.
*
* Based on {@code TestServer} of the FS Internet Radio Binding.
*
* @author Velin Yordanov - initial contribution
* @author Wouter Born - Increase test coverage
*/
public class NestTestServer {
private final Logger logger = LoggerFactory.getLogger(NestTestServer.class);
private Server server;
private String host;
private int port;
private int timeout;
private ServletHolder servletHolder;
public NestTestServer(String host, int port, int timeout, ServletHolder servletHolder) {
this.host = host;
this.port = port;
this.timeout = timeout;
this.servletHolder = servletHolder;
}
public void startServer() {
Thread thread = new Thread(new Runnable() {
@Override
@SuppressWarnings("resource")
public void run() {
server = new Server();
ServletHandler handler = new ServletHandler();
handler.addServletWithMapping(servletHolder, "/*");
server.setHandler(handler);
// HTTP connector
ServerConnector http = new ServerConnector(server);
http.setHost(host);
http.setPort(port);
http.setIdleTimeout(timeout);
server.addConnector(http);
try {
server.start();
server.join();
} catch (InterruptedException ex) {
logger.error("Server got interrupted", ex);
return;
} catch (Exception e) {
logger.error("Error in starting the server", e);
return;
}
}
});
thread.start();
}
public void stopServer() {
try {
server.stop();
} catch (Exception e) {
logger.error("Error in stopping the server", e);
return;
}
}
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="nest"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="test_account">
<label>Test Account</label>
<description>An account for testing the Nest binding</description>
<config-description-ref uri="thing-type:nest:account"/>
</bridge-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,4 @@
{
"access_token": "access_token",
"expires_in": 315360000
}

View File

@@ -0,0 +1,33 @@
{
"app_url": "https://camera_app_url",
"device_id": "_LK8j9rRXwCKEBOtDo7JskNxzWfHBOIm3CLouCT3FQZzrvokK_DzFQ",
"is_audio_input_enabled": true,
"is_online": true,
"is_public_share_enabled": false,
"is_streaming": false,
"is_video_history_enabled": false,
"last_event": {
"activity_zone_ids": [
"id1",
"id2"
],
"animated_image_url": "https://last_event_animated_image_url",
"app_url": "https://last_event_app_url",
"end_time": "2017-01-22T07:40:38.680Z",
"has_motion": true,
"has_person": false,
"has_sound": false,
"image_url": "https://last_event_image_url",
"start_time": "2017-01-22T07:40:19.020Z",
"urls_expire_time": "2017-02-05T07:40:19.020Z",
"web_url": "https://last_event_web_url"
},
"last_is_online_change": "2017-01-22T08:19:20.000Z",
"name": "Upstairs",
"name_long": "Upstairs Camera",
"snapshot_url": "https://camera_snapshot_url",
"software_version": "205-600052",
"structure_id": "ysCnsCaq1pQwKUPP9H4AqE943C1XtLin3x6uCVN5Qh09IDyTg7Ey5A",
"web_url": "https://camera_web_url",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKCxvyZfxNpKA"
}

View File

@@ -0,0 +1,6 @@
{
"error": "blocked",
"type": "https://developer.nest.com/documentation/cloud/error-messages#blocked",
"message": "blocked",
"instance": "bb514046-edc9-4bca-8239-f7a3cfb0925a"
}

View File

@@ -0,0 +1,17 @@
{
"battery_health": "ok",
"co_alarm_state": "ok",
"device_id": "p1b1oySOcs_sbi4iczruW3Ou-iQr8PMV",
"is_manual_test_active": false,
"is_online": true,
"last_connection": "2017-02-02T20:53:05.338Z",
"locale": "en-US",
"name": "Downstairs",
"name_long": "Downstairs Nest Protect",
"smoke_alarm_state": "ok",
"software_version": "3.1rc9",
"structure_id": "ysCnsCaq1pQwKUPP9H4AqE943C1XtLin3x6uCVN5Qh09IDyTg7Ey5A",
"ui_color_state": "green",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIm5E0NfJPeeg",
"where_name": "Downstairs"
}

View File

@@ -0,0 +1,112 @@
{
"smoke_co_alarms": [
"p1b1oySOcs-OJHIgmgeMkHOu-iQr8PMV",
"p1b1oySOcs8Qu7IAJVrQ7XOu-iQr8PMV",
"p1b1oySOcs8W9WwaNu80oXOu-iQr8PMV",
"p1b1oySOcs_sbi4iczruW3Ou-iQr8PMV"
],
"name": "Home",
"country_code": "US",
"postal_code": "98056",
"time_zone": "America/Los_Angeles",
"away": "home",
"thermostats": [
"G1jouHN5yl6mXFaQw5iGwXOu-iQr8PMV"
],
"structure_id": "ysCnsCaq1pQwKUPP9H4AqE943C1XtLin3x6uCVN5Qh09IDyTg7Ey5A",
"rhr_enrollment": false,
"co_alarm_state": "ok",
"smoke_alarm_state": "ok",
"eta_begin": "2017-02-02T03:10:08.000Z",
"wwn_security_state": "ok",
"wheres": {
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIYpqdaXnYjUg": {
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIYpqdaXnYjUg",
"name": "Basement"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsK-nCnEjccnMQ": {
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsK-nCnEjccnMQ",
"name": "Bedroom"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsJyRQEOtmKqkw": {
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsJyRQEOtmKqkw",
"name": "Den"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKZphUIYeW39g": {
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKZphUIYeW39g",
"name": "Dining Room"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIm5E0NfJPeeg": {
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIm5E0NfJPeeg",
"name": "Downstairs"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsK2kdsXRP3IFg": {
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsK2kdsXRP3IFg",
"name": "Entryway"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIAYVvcpN1cOA": {
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIAYVvcpN1cOA",
"name": "Family Room"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIB7GULj0y7Rw": {
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIB7GULj0y7Rw",
"name": "Hallway"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIbTUmML4Q6xA": {
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIbTUmML4Q6xA",
"name": "Kids Room"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIB2f05cPKRBA": {
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIB2f05cPKRBA",
"name": "Kitchen"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKQrCrjN0yXiw": {
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKQrCrjN0yXiw",
"name": "Living Room"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIebdVzhA62Iw": {
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIebdVzhA62Iw",
"name": "Master Bedroom"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKtUyRb3je64Q": {
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKtUyRb3je64Q",
"name": "Office"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKCxvyZfxNpKA": {
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKCxvyZfxNpKA",
"name": "Upstairs"
},
"6UAWzz8czKpFrH6EK3AcjDiTjbRgts8x5MJxEnn1yKKQpYTBO7n2UQ": {
"where_id": "6UAWzz8czKpFrH6EK3AcjDiTjbRgts8x5MJxEnn1yKKQpYTBO7n2UQ",
"name": "Downstairs Kitchen"
},
"qpWvTu89Knhn6GRFM-VtGoE4KYwbzbJg9INR6WyPfhW1EJ04GRyYbQ": {
"where_id": "qpWvTu89Knhn6GRFM-VtGoE4KYwbzbJg9INR6WyPfhW1EJ04GRyYbQ",
"name": "Garage"
},
"8tH6YiXUAQDZFLD6AgMmQ14Sc5wTG0NxKfabPY0XKrqc47t3uSDZvQ": {
"where_id": "8tH6YiXUAQDZFLD6AgMmQ14Sc5wTG0NxKfabPY0XKrqc47t3uSDZvQ",
"name": "Frog"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKfexoqPTcUVA": {
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKfexoqPTcUVA",
"name": "Backyard"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsJv12iEHQ0hxA": {
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsJv12iEHQ0hxA",
"name": "Driveway"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsLRu9lIioI47g": {
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsLRu9lIioI47g",
"name": "Front Yard"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKR8TWb9hTptQ": {
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKR8TWb9hTptQ",
"name": "Outside"
}
},
"cameras": [
"_LK8j9rRXwCKEBOtDo7JskNxzWfHBOIm3CLouCT3FQZzrvokK_DzFQ",
"VG7C7BU6Zf8OjEfizmBCVnwnuKHSnOBIHgbQKa57xKJzrvokK_DzFQ"
]
}

View File

@@ -0,0 +1,51 @@
{
"ambient_temperature_c": 19.0,
"ambient_temperature_f": 66,
"away_temperature_high_c": 24.0,
"away_temperature_high_f": 76,
"away_temperature_low_c": 12.5,
"away_temperature_low_f": 55,
"can_cool": false,
"can_heat": true,
"device_id": "G1jouHN5yl6mXFaQw5iGwXOu-iQr8PMV",
"eco_temperature_high_c": 24.0,
"eco_temperature_high_f": 76,
"eco_temperature_low_c": 12.5,
"eco_temperature_low_f": 55,
"fan_timer_active": false,
"fan_timer_duration": 15,
"fan_timer_timeout": "1970-01-01T00:00:00.000Z",
"has_fan": true,
"has_leaf": true,
"humidity": 25,
"hvac_mode": "heat",
"hvac_state": "off",
"is_locked": false,
"is_online": true,
"is_using_emergency_heat": false,
"label": "Living Room",
"last_connection": "2017-02-02T21:00:06.000Z",
"locale": "en-GB",
"locked_temp_max_c": 22.0,
"locked_temp_max_f": 72,
"locked_temp_min_c": 20.0,
"locked_temp_min_f": 68,
"name": "Living Room (Living Room)",
"name_long": "Living Room Thermostat (Living Room)",
"previous_hvac_mode": "",
"software_version": "5.6-7",
"structure_id": "ysCnsCaq1pQwKUPP9H4AqE943C1XtLin3x6uCVN5Qh09IDyTg7Ey5A",
"sunlight_correction_active": false,
"sunlight_correction_enabled": true,
"target_temperature_c": 15.5,
"target_temperature_f": 60,
"target_temperature_high_c": 24.0,
"target_temperature_high_f": 75,
"target_temperature_low_c": 20.0,
"target_temperature_low_f": 68,
"temperature_scale": "C",
"time_to_target": "~0",
"time_to_target_training": "ready",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKQrCrjN0yXiw",
"where_name": "Living Room"
}

View File

@@ -0,0 +1,307 @@
{
"devices": {
"cameras": {
"_LK8j9rRXwCKEBOtDo7JskNxzWfHBOIm3CLouCT3FQZzrvokK_DzFQ": {
"app_url": "https://camera_app_url",
"device_id": "_LK8j9rRXwCKEBOtDo7JskNxzWfHBOIm3CLouCT3FQZzrvokK_DzFQ",
"is_audio_input_enabled": true,
"is_online": false,
"is_public_share_enabled": false,
"is_streaming": false,
"is_video_history_enabled": false,
"last_event": {
"activity_zone_ids": [
"id1",
"id2"
],
"animated_image_url": "https://last_event_animated_image_url",
"app_url": "https://last_event_app_url",
"end_time": "2017-01-22T07:40:38.680Z",
"has_motion": true,
"has_person": false,
"has_sound": false,
"image_url": "https://last_event_image_url",
"start_time": "2017-01-22T07:40:19.020Z",
"urls_expire_time": "2017-02-05T07:40:19.020Z",
"web_url": "https://last_event_web_url"
},
"last_is_online_change": "2017-01-22T08:19:20.000Z",
"name": "Upstairs",
"name_long": "Upstairs Camera",
"snapshot_url": "https://camera_snapshot_url",
"software_version": "205-600052",
"structure_id": "ysCnsCaq1pQwKUPP9H4AqE943C1XtLin3x6uCVN5Qh09IDyTg7Ey5A",
"web_url": "https://camera_web_url",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKCxvyZfxNpKA"
},
"VG7C7BU6Zf8OjEfizmBCVnwnuKHSnOBIHgbQKa57xKJzrvokK_DzFQ": {
"app_url": "nestmobile://cameras/CjZWRzdDN0JVNlpmOE9qRWZpem1CQ1Zud251S0hTbk9CSUhnYlFLYTU3eEtKenJ2b2tLX0R6RlESFm9wNVB2NW93NmJ6cUdvMkZQSGUxdEEaNld0Mkl5b2tIR0tKX2FpUVd1SkRnQjc2ejhSWFl3SFFxWXFrSWx2QlpxN1gyeWNqdmRZVjdGQQ?auth=c.eQ5QBBPiFOTNzPHbmZPcE9yPZ7GayzLusifgQR2DQRFNyUS9ESvlhJF0D7vG8Y0TFV39zX1vIOsWrv8RKCMrFepNUb9FqHEboa4MtWLUsGb4tD9oBh0jrV4HooJUmz5sVA5KZR0dkxyLYyPc",
"device_id": "VG7C7BU6Zf8OjEfizmBCVnwnuKHSnOBIHgbQKa57xKJzrvokK_DzFQ",
"is_audio_input_enabled": true,
"is_online": false,
"is_public_share_enabled": false,
"is_streaming": false,
"is_video_history_enabled": false,
"last_event": {
"end_time": "2016-11-20T07:02:46.860Z",
"has_motion": true,
"has_person": false,
"has_sound": false,
"start_time": "2016-11-20T07:02:27.260Z"
},
"last_is_online_change": "2016-11-20T07:03:42.000Z",
"name": "Garage",
"name_long": "Garage Camera",
"snapshot_url": "https://www.dropcam.com/api/wwn.get_snapshot/CjZWRzdDN0JVNlpmOE9qRWZpem1CQ1Zud251S0hTbk9CSUhnYlFLYTU3eEtKenJ2b2tLX0R6RlESFm9wNVB2NW93NmJ6cUdvMkZQSGUxdEEaNld0Mkl5b2tIR0tKX2FpUVd1SkRnQjc2ejhSWFl3SFFxWXFrSWx2QlpxN1gyeWNqdmRZVjdGQQ?auth=c.eQ5QBBPiFOTNzPHbmZPcE9yPZ7GayzLusifgQR2DQRFNyUS9ESvlhJF0D7vG8Y0TFV39zX1vIOsWrv8RKCMrFepNUb9FqHEboa4MtWLUsGb4tD9oBh0jrV4HooJUmz5sVA5KZR0dkxyLYyPc",
"software_version": "205-600052",
"structure_id": "ysCnsCaq1pQwKUPP9H4AqE943C1XtLin3x6uCVN5Qh09IDyTg7Ey5A",
"web_url": "https://home.nest.com/cameras/CjZWRzdDN0JVNlpmOE9qRWZpem1CQ1Zud251S0hTbk9CSUhnYlFLYTU3eEtKenJ2b2tLX0R6RlESFm9wNVB2NW93NmJ6cUdvMkZQSGUxdEEaNld0Mkl5b2tIR0tKX2FpUVd1SkRnQjc2ejhSWFl3SFFxWXFrSWx2QlpxN1gyeWNqdmRZVjdGQQ?auth=c.eQ5QBBPiFOTNzPHbmZPcE9yPZ7GayzLusifgQR2DQRFNyUS9ESvlhJF0D7vG8Y0TFV39zX1vIOsWrv8RKCMrFepNUb9FqHEboa4MtWLUsGb4tD9oBh0jrV4HooJUmz5sVA5KZR0dkxyLYyPc",
"where_id": "qpWvTu89Knhn6GRFM-VtGoE4KYwbzbJg9INR6WyPfhW1EJ04GRyYbQ"
}
},
"smoke_co_alarms": {
"p1b1oySOcs_sbi4iczruW3Ou-iQr8PMV": {
"battery_health": "ok",
"co_alarm_state": "ok",
"device_id": "p1b1oySOcs_sbi4iczruW3Ou-iQr8PMV",
"is_manual_test_active": false,
"is_online": true,
"last_connection": "2017-02-02T20:53:05.338Z",
"locale": "en-US",
"name": "Downstairs",
"name_long": "Downstairs Nest Protect",
"smoke_alarm_state": "ok",
"software_version": "3.1rc9",
"structure_id": "ysCnsCaq1pQwKUPP9H4AqE943C1XtLin3x6uCVN5Qh09IDyTg7Ey5A",
"ui_color_state": "green",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIm5E0NfJPeeg",
"where_name": "Downstairs"
},
"p1b1oySOcs8W9WwaNu80oXOu-iQr8PMV": {
"battery_health": "ok",
"co_alarm_state": "ok",
"device_id": "p1b1oySOcs8W9WwaNu80oXOu-iQr8PMV",
"is_manual_test_active": false,
"is_online": true,
"last_connection": "2017-02-02T20:35:50.051Z",
"last_manual_test_time": "1970-01-01T00:00:00.000Z",
"locale": "en-US",
"name": "Upstairs",
"name_long": "Upstairs Nest Protect",
"smoke_alarm_state": "ok",
"software_version": "3.1rc9",
"structure_id": "ysCnsCaq1pQwKUPP9H4AqE943C1XtLin3x6uCVN5Qh09IDyTg7Ey5A",
"ui_color_state": "green",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKCxvyZfxNpKA",
"where_name": "Upstairs"
},
"p1b1oySOcs-OJHIgmgeMkHOu-iQr8PMV": {
"battery_health": "ok",
"co_alarm_state": "ok",
"device_id": "p1b1oySOcs-OJHIgmgeMkHOu-iQr8PMV",
"is_manual_test_active": false,
"is_online": true,
"last_connection": "2017-02-02T11:04:18.804Z",
"last_manual_test_time": "1970-01-01T00:00:00.000Z",
"locale": "en-US",
"name": "Downstairs Kitchen",
"name_long": "Downstairs Kitchen Nest Protect",
"smoke_alarm_state": "ok",
"software_version": "3.1rc9",
"structure_id": "ysCnsCaq1pQwKUPP9H4AqE943C1XtLin3x6uCVN5Qh09IDyTg7Ey5A",
"ui_color_state": "green",
"where_id": "6UAWzz8czKpFrH6EK3AcjDiTjbRgts8x5MJxEnn1yKKQpYTBO7n2UQ",
"where_name": "Downstairs Kitchen"
},
"p1b1oySOcs8Qu7IAJVrQ7XOu-iQr8PMV": {
"battery_health": "ok",
"co_alarm_state": "ok",
"device_id": "p1b1oySOcs8Qu7IAJVrQ7XOu-iQr8PMV",
"is_manual_test_active": false,
"is_online": true,
"last_connection": "2017-02-02T13:30:34.187Z",
"last_manual_test_time": "1970-01-01T00:00:00.000Z",
"locale": "en-US",
"name": "Living Room",
"name_long": "Living Room Nest Protect",
"smoke_alarm_state": "ok",
"software_version": "3.1rc9",
"structure_id": "ysCnsCaq1pQwKUPP9H4AqE943C1XtLin3x6uCVN5Qh09IDyTg7Ey5A",
"ui_color_state": "green",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKQrCrjN0yXiw",
"where_name": "Living Room"
}
},
"thermostats": {
"G1jouHN5yl6mXFaQw5iGwXOu-iQr8PMV": {
"ambient_temperature_c": 19.0,
"ambient_temperature_f": 66,
"away_temperature_high_c": 24.0,
"away_temperature_high_f": 76,
"away_temperature_low_c": 12.5,
"away_temperature_low_f": 55,
"can_cool": false,
"can_heat": true,
"device_id": "G1jouHN5yl6mXFaQw5iGwXOu-iQr8PMV",
"eco_temperature_high_c": 24.0,
"eco_temperature_high_f": 76,
"eco_temperature_low_c": 12.5,
"eco_temperature_low_f": 55,
"fan_timer_active": false,
"fan_timer_duration": 15,
"fan_timer_timeout": "1970-01-01T00:00:00.000Z",
"has_fan": true,
"has_leaf": true,
"humidity": 25,
"hvac_mode": "heat",
"hvac_state": "off",
"is_locked": false,
"is_online": true,
"is_using_emergency_heat": false,
"label": "Living Room",
"last_connection": "2017-02-02T21:00:06.000Z",
"locale": "en-GB",
"locked_temp_max_c": 22.0,
"locked_temp_max_f": 72,
"locked_temp_min_c": 20.0,
"locked_temp_min_f": 68,
"name": "Living Room (Living Room)",
"name_long": "Living Room Thermostat (Living Room)",
"previous_hvac_mode": "",
"software_version": "5.6-7",
"structure_id": "ysCnsCaq1pQwKUPP9H4AqE943C1XtLin3x6uCVN5Qh09IDyTg7Ey5A",
"sunlight_correction_active": false,
"sunlight_correction_enabled": true,
"target_temperature_c": 15.5,
"target_temperature_f": 60,
"target_temperature_high_c": 24.0,
"target_temperature_high_f": 75,
"target_temperature_low_c": 20.0,
"target_temperature_low_f": 68,
"temperature_scale": "C",
"time_to_target": "~0",
"time_to_target_training": "ready",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKQrCrjN0yXiw",
"where_name": "Living Room"
}
}
},
"metadata": {
"access_token": "c.eQ5QBBPiFOTNzPHbmZPcE9yPZ7GayzLusifgQR2DQRFNyUS9ESvlhJF0D7vG8Y0TFV39zX1vIOsWrv8RKCMrFepNUb9FqHEboa4MtWLUsGb4tD9oBh0jrV4HooJUmz5sVA5KZR0dkxyLYyPc",
"client_version": 1
},
"structures": {
"ysCnsCaq1pQwKUPP9H4AqE943C1XtLin3x6uCVN5Qh09IDyTg7Ey5A": {
"away": "home",
"cameras": [
"_LK8j9rRXwCKEBOtDo7JskNxzWfHBOIm3CLouCT3FQZzrvokK_DzFQ",
"VG7C7BU6Zf8OjEfizmBCVnwnuKHSnOBIHgbQKa57xKJzrvokK_DzFQ"
],
"co_alarm_state": "ok",
"country_code": "US",
"eta_begin": "2017-02-02T03:10:08.000Z",
"name": "Home",
"postal_code": "98056",
"rhr_enrollment": false,
"smoke_alarm_state": "ok",
"smoke_co_alarms": [
"p1b1oySOcs-OJHIgmgeMkHOu-iQr8PMV",
"p1b1oySOcs8Qu7IAJVrQ7XOu-iQr8PMV",
"p1b1oySOcs8W9WwaNu80oXOu-iQr8PMV",
"p1b1oySOcs_sbi4iczruW3Ou-iQr8PMV"
],
"structure_id": "ysCnsCaq1pQwKUPP9H4AqE943C1XtLin3x6uCVN5Qh09IDyTg7Ey5A",
"thermostats": [
"G1jouHN5yl6mXFaQw5iGwXOu-iQr8PMV"
],
"time_zone": "America/Los_Angeles",
"wheres": {
"6UAWzz8czKpFrH6EK3AcjDiTjbRgts8x5MJxEnn1yKKQpYTBO7n2UQ": {
"name": "Downstairs Kitchen",
"where_id": "6UAWzz8czKpFrH6EK3AcjDiTjbRgts8x5MJxEnn1yKKQpYTBO7n2UQ"
},
"8tH6YiXUAQDZFLD6AgMmQ14Sc5wTG0NxKfabPY0XKrqc47t3uSDZvQ": {
"name": "Frog",
"where_id": "8tH6YiXUAQDZFLD6AgMmQ14Sc5wTG0NxKfabPY0XKrqc47t3uSDZvQ"
},
"qpWvTu89Knhn6GRFM-VtGoE4KYwbzbJg9INR6WyPfhW1EJ04GRyYbQ": {
"name": "Garage",
"where_id": "qpWvTu89Knhn6GRFM-VtGoE4KYwbzbJg9INR6WyPfhW1EJ04GRyYbQ"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIAYVvcpN1cOA": {
"name": "Family Room",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIAYVvcpN1cOA"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIB2f05cPKRBA": {
"name": "Kitchen",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIB2f05cPKRBA"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIB7GULj0y7Rw": {
"name": "Hallway",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIB7GULj0y7Rw"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIYpqdaXnYjUg": {
"name": "Basement",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIYpqdaXnYjUg"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIbTUmML4Q6xA": {
"name": "Kids Room",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIbTUmML4Q6xA"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIebdVzhA62Iw": {
"name": "Master Bedroom",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIebdVzhA62Iw"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIm5E0NfJPeeg": {
"name": "Downstairs",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIm5E0NfJPeeg"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsJv12iEHQ0hxA": {
"name": "Driveway",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsJv12iEHQ0hxA"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsJyRQEOtmKqkw": {
"name": "Den",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsJyRQEOtmKqkw"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsK-nCnEjccnMQ": {
"name": "Bedroom",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsK-nCnEjccnMQ"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsK2kdsXRP3IFg": {
"name": "Entryway",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsK2kdsXRP3IFg"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKCxvyZfxNpKA": {
"name": "Upstairs",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKCxvyZfxNpKA"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKQrCrjN0yXiw": {
"name": "Living Room",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKQrCrjN0yXiw"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKR8TWb9hTptQ": {
"name": "Outside",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKR8TWb9hTptQ"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKZphUIYeW39g": {
"name": "Dining Room",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKZphUIYeW39g"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKfexoqPTcUVA": {
"name": "Backyard",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKfexoqPTcUVA"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKtUyRb3je64Q": {
"name": "Office",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKtUyRb3je64Q"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsLRu9lIioI47g": {
"name": "Front Yard",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsLRu9lIioI47g"
}
},
"wwn_security_state": "ok"
}
}
}

View File

@@ -0,0 +1,45 @@
{
"path": "/",
"data": {
"devices": {
"cameras": {
"_LK8j9rRXwCKEBOtDo7JskNxzWfHBOIm3CLouCT3FQZzrvokK_DzFQ": {
"device_id": "_LK8j9rRXwCKEBOtDo7JskNxzWfHBOIm3CLouCT3FQZzrvokK_DzFQ"
},
"VG7C7BU6Zf8OjEfizmBCVnwnuKHSnOBIHgbQKa57xKJzrvokK_DzFQ": {
"device_id": "VG7C7BU6Zf8OjEfizmBCVnwnuKHSnOBIHgbQKa57xKJzrvokK_DzFQ"
}
},
"smoke_co_alarms": {
"p1b1oySOcs_sbi4iczruW3Ou-iQr8PMV": {
"device_id": "p1b1oySOcs_sbi4iczruW3Ou-iQr8PMV"
},
"p1b1oySOcs8W9WwaNu80oXOu-iQr8PMV": {
"device_id": "p1b1oySOcs8W9WwaNu80oXOu-iQr8PMV"
},
"p1b1oySOcs-OJHIgmgeMkHOu-iQr8PMV": {
"device_id": "p1b1oySOcs-OJHIgmgeMkHOu-iQr8PMV"
},
"p1b1oySOcs8Qu7IAJVrQ7XOu-iQr8PMV": {
"device_id": "p1b1oySOcs8Qu7IAJVrQ7XOu-iQr8PMV"
}
},
"thermostats": {
"G1jouHN5yl6mXFaQw5iGwXOu-iQr8PMV": {
"device_id": "G1jouHN5yl6mXFaQw5iGwXOu-iQr8PMV"
},
"OTQoylk2h5Ld3cfpm3esR0qx-iQr8PMV": {
"device_id": "OTQoylk2h5Ld3cfpm3esR0qx-iQr8PMV"
}
}
},
"structures": {
"ysCnsCaq1pQwKUPP9H4AqE943C1XtLin3x6uCVN5Qh09IDyTg7Ey5A": {
"structure_id": "ysCnsCaq1pQwKUPP9H4AqE943C1XtLin3x6uCVN5Qh09IDyTg7Ey5A"
},
"SylKI7puaWd56ILAcJ46LzmtdZc3L4wGzScs8yLc5zccJofBIW9KTJ": {
"structure_id": "SylKI7puaWd56ILAcJ46LzmtdZc3L4wGzScs8yLc5zccJofBIW9KTJ"
}
}
}
}

View File

@@ -0,0 +1,314 @@
{
"path": "/",
"data": {
"devices": {
"cameras": {
"_LK8j9rRXwCKEBOtDo7JskNxzWfHBOIm3CLouCT3FQZzrvokK_DzFQ": {
"app_url": "https://camera_app_url",
"device_id": "_LK8j9rRXwCKEBOtDo7JskNxzWfHBOIm3CLouCT3FQZzrvokK_DzFQ",
"is_audio_input_enabled": true,
"is_online": true,
"is_public_share_enabled": false,
"is_streaming": false,
"is_video_history_enabled": false,
"last_event": {
"activity_zone_ids": [
"id1",
"id2"
],
"animated_image_url": "https://last_event_animated_image_url",
"app_url": "https://last_event_app_url",
"end_time": "2017-01-22T07:40:38.680Z",
"has_motion": true,
"has_person": false,
"has_sound": false,
"image_url": "https://last_event_image_url",
"start_time": "2017-01-22T07:40:19.020Z",
"urls_expire_time": "2017-02-05T07:40:19.020Z",
"web_url": "https://last_event_web_url"
},
"last_is_online_change": "2017-01-22T08:19:20.000Z",
"name": "Upstairs",
"name_long": "Upstairs Camera",
"public_share_url": "https://camera_public_share_url",
"snapshot_url": "https://camera_snapshot_url",
"software_version": "205-600052",
"structure_id": "ysCnsCaq1pQwKUPP9H4AqE943C1XtLin3x6uCVN5Qh09IDyTg7Ey5A",
"web_url": "https://camera_web_url",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKCxvyZfxNpKA"
},
"VG7C7BU6Zf8OjEfizmBCVnwnuKHSnOBIHgbQKa57xKJzrvokK_DzFQ": {
"app_url": "nestmobile://cameras/CjZWRzdDN0JVNlpmOE9qRWZpem1CQ1Zud251S0hTbk9CSUhnYlFLYTU3eEtKenJ2b2tLX0R6RlESFm9wNVB2NW93NmJ6cUdvMkZQSGUxdEEaNld0Mkl5b2tIR0tKX2FpUVd1SkRnQjc2ejhSWFl3SFFxWXFrSWx2QlpxN1gyeWNqdmRZVjdGQQ?auth=c.eQ5QBBPiFOTNzPHbmZPcE9yPZ7GayzLusifgQR2DQRFNyUS9ESvlhJF0D7vG8Y0TFV39zX1vIOsWrv8RKCMrFepNUb9FqHEboa4MtWLUsGb4tD9oBh0jrV4HooJUmz5sVA5KZR0dkxyLYyPc",
"device_id": "VG7C7BU6Zf8OjEfizmBCVnwnuKHSnOBIHgbQKa57xKJzrvokK_DzFQ",
"is_audio_input_enabled": true,
"is_online": false,
"is_public_share_enabled": false,
"is_streaming": false,
"is_video_history_enabled": false,
"last_event": {
"end_time": "2016-11-20T07:02:46.860Z",
"has_motion": true,
"has_person": false,
"has_sound": false,
"start_time": "2016-11-20T07:02:27.260Z"
},
"last_is_online_change": "2016-11-20T07:03:42.000Z",
"name": "Garage",
"name_long": "Garage Camera",
"snapshot_url": "https://www.dropcam.com/api/wwn.get_snapshot/CjZWRzdDN0JVNlpmOE9qRWZpem1CQ1Zud251S0hTbk9CSUhnYlFLYTU3eEtKenJ2b2tLX0R6RlESFm9wNVB2NW93NmJ6cUdvMkZQSGUxdEEaNld0Mkl5b2tIR0tKX2FpUVd1SkRnQjc2ejhSWFl3SFFxWXFrSWx2QlpxN1gyeWNqdmRZVjdGQQ?auth=c.eQ5QBBPiFOTNzPHbmZPcE9yPZ7GayzLusifgQR2DQRFNyUS9ESvlhJF0D7vG8Y0TFV39zX1vIOsWrv8RKCMrFepNUb9FqHEboa4MtWLUsGb4tD9oBh0jrV4HooJUmz5sVA5KZR0dkxyLYyPc",
"software_version": "205-600052",
"structure_id": "ysCnsCaq1pQwKUPP9H4AqE943C1XtLin3x6uCVN5Qh09IDyTg7Ey5A",
"web_url": "https://home.nest.com/cameras/CjZWRzdDN0JVNlpmOE9qRWZpem1CQ1Zud251S0hTbk9CSUhnYlFLYTU3eEtKenJ2b2tLX0R6RlESFm9wNVB2NW93NmJ6cUdvMkZQSGUxdEEaNld0Mkl5b2tIR0tKX2FpUVd1SkRnQjc2ejhSWFl3SFFxWXFrSWx2QlpxN1gyeWNqdmRZVjdGQQ?auth=c.eQ5QBBPiFOTNzPHbmZPcE9yPZ7GayzLusifgQR2DQRFNyUS9ESvlhJF0D7vG8Y0TFV39zX1vIOsWrv8RKCMrFepNUb9FqHEboa4MtWLUsGb4tD9oBh0jrV4HooJUmz5sVA5KZR0dkxyLYyPc",
"where_id": "qpWvTu89Knhn6GRFM-VtGoE4KYwbzbJg9INR6WyPfhW1EJ04GRyYbQ"
}
},
"smoke_co_alarms": {
"p1b1oySOcs_sbi4iczruW3Ou-iQr8PMV": {
"battery_health": "ok",
"co_alarm_state": "ok",
"device_id": "p1b1oySOcs_sbi4iczruW3Ou-iQr8PMV",
"is_manual_test_active": false,
"is_online": true,
"last_connection": "2017-02-02T20:53:05.338Z",
"last_manual_test_time": "2016-10-31T23:59:59.000Z",
"locale": "en-US",
"name": "Downstairs",
"name_long": "Downstairs Nest Protect",
"smoke_alarm_state": "ok",
"software_version": "3.1rc9",
"structure_id": "ysCnsCaq1pQwKUPP9H4AqE943C1XtLin3x6uCVN5Qh09IDyTg7Ey5A",
"ui_color_state": "green",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIm5E0NfJPeeg",
"where_name": "Downstairs"
},
"p1b1oySOcs8W9WwaNu80oXOu-iQr8PMV": {
"battery_health": "ok",
"co_alarm_state": "ok",
"device_id": "p1b1oySOcs8W9WwaNu80oXOu-iQr8PMV",
"is_manual_test_active": false,
"is_online": true,
"last_connection": "2017-02-02T20:35:50.051Z",
"last_manual_test_time": "1970-01-01T00:00:00.000Z",
"locale": "en-US",
"name": "Upstairs",
"name_long": "Upstairs Nest Protect",
"smoke_alarm_state": "ok",
"software_version": "3.1rc9",
"structure_id": "ysCnsCaq1pQwKUPP9H4AqE943C1XtLin3x6uCVN5Qh09IDyTg7Ey5A",
"ui_color_state": "green",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKCxvyZfxNpKA",
"where_name": "Upstairs"
},
"p1b1oySOcs-OJHIgmgeMkHOu-iQr8PMV": {
"battery_health": "ok",
"co_alarm_state": "ok",
"device_id": "p1b1oySOcs-OJHIgmgeMkHOu-iQr8PMV",
"is_manual_test_active": false,
"is_online": true,
"last_connection": "2017-02-02T11:04:18.804Z",
"last_manual_test_time": "1970-01-01T00:00:00.000Z",
"locale": "en-US",
"name": "Downstairs Kitchen",
"name_long": "Downstairs Kitchen Nest Protect",
"smoke_alarm_state": "ok",
"software_version": "3.1rc9",
"structure_id": "ysCnsCaq1pQwKUPP9H4AqE943C1XtLin3x6uCVN5Qh09IDyTg7Ey5A",
"ui_color_state": "green",
"where_id": "6UAWzz8czKpFrH6EK3AcjDiTjbRgts8x5MJxEnn1yKKQpYTBO7n2UQ",
"where_name": "Downstairs Kitchen"
},
"p1b1oySOcs8Qu7IAJVrQ7XOu-iQr8PMV": {
"battery_health": "ok",
"co_alarm_state": "ok",
"device_id": "p1b1oySOcs8Qu7IAJVrQ7XOu-iQr8PMV",
"is_manual_test_active": false,
"is_online": true,
"last_connection": "2017-02-02T13:30:34.187Z",
"last_manual_test_time": "1970-01-01T00:00:00.000Z",
"locale": "en-US",
"name": "Living Room",
"name_long": "Living Room Nest Protect",
"smoke_alarm_state": "ok",
"software_version": "3.1rc9",
"structure_id": "ysCnsCaq1pQwKUPP9H4AqE943C1XtLin3x6uCVN5Qh09IDyTg7Ey5A",
"ui_color_state": "green",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKQrCrjN0yXiw",
"where_name": "Living Room"
}
},
"thermostats": {
"G1jouHN5yl6mXFaQw5iGwXOu-iQr8PMV": {
"ambient_temperature_c": 19.0,
"ambient_temperature_f": 66,
"away_temperature_high_c": 24.0,
"away_temperature_high_f": 76,
"away_temperature_low_c": 12.5,
"away_temperature_low_f": 55,
"can_cool": false,
"can_heat": true,
"device_id": "G1jouHN5yl6mXFaQw5iGwXOu-iQr8PMV",
"eco_temperature_high_c": 24.0,
"eco_temperature_high_f": 76,
"eco_temperature_low_c": 12.5,
"eco_temperature_low_f": 55,
"fan_timer_active": false,
"fan_timer_duration": 15,
"fan_timer_timeout": "1970-01-01T00:00:00.000Z",
"has_fan": true,
"has_leaf": true,
"humidity": 25,
"hvac_mode": "heat",
"hvac_state": "off",
"is_locked": false,
"is_online": true,
"is_using_emergency_heat": false,
"label": "Living Room",
"last_connection": "2017-02-02T21:00:06.000Z",
"locale": "en-GB",
"locked_temp_max_c": 22.0,
"locked_temp_max_f": 72,
"locked_temp_min_c": 20.0,
"locked_temp_min_f": 68,
"name": "Living Room (Living Room)",
"name_long": "Living Room Thermostat (Living Room)",
"previous_hvac_mode": "",
"software_version": "5.6-7",
"structure_id": "ysCnsCaq1pQwKUPP9H4AqE943C1XtLin3x6uCVN5Qh09IDyTg7Ey5A",
"sunlight_correction_active": false,
"sunlight_correction_enabled": true,
"target_temperature_c": 15.5,
"target_temperature_f": 60,
"target_temperature_high_c": 24.0,
"target_temperature_high_f": 75,
"target_temperature_low_c": 20.0,
"target_temperature_low_f": 68,
"temperature_scale": "C",
"time_to_target": "~0",
"time_to_target_training": "ready",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKQrCrjN0yXiw",
"where_name": "Living Room"
}
}
},
"metadata": {
"access_token": "c.eQ5QBBPiFOTNzPHbmZPcE9yPZ7GayzLusifgQR2DQRFNyUS9ESvlhJF0D7vG8Y0TFV39zX1vIOsWrv8RKCMrFepNUb9FqHEboa4MtWLUsGb4tD9oBh0jrV4HooJUmz5sVA5KZR0dkxyLYyPc",
"client_version": 1
},
"structures": {
"ysCnsCaq1pQwKUPP9H4AqE943C1XtLin3x6uCVN5Qh09IDyTg7Ey5A": {
"away": "home",
"cameras": [
"_LK8j9rRXwCKEBOtDo7JskNxzWfHBOIm3CLouCT3FQZzrvokK_DzFQ",
"VG7C7BU6Zf8OjEfizmBCVnwnuKHSnOBIHgbQKa57xKJzrvokK_DzFQ"
],
"co_alarm_state": "ok",
"country_code": "US",
"eta_begin": "2017-02-02T03:10:08.000Z",
"name": "Home",
"peak_period_end_time": "2017-07-01T01:03:08.400Z",
"peak_period_start_time": "2017-06-01T13:31:10.870Z",
"postal_code": "98056",
"rhr_enrollment": false,
"smoke_alarm_state": "ok",
"smoke_co_alarms": [
"p1b1oySOcs-OJHIgmgeMkHOu-iQr8PMV",
"p1b1oySOcs8Qu7IAJVrQ7XOu-iQr8PMV",
"p1b1oySOcs8W9WwaNu80oXOu-iQr8PMV",
"p1b1oySOcs_sbi4iczruW3Ou-iQr8PMV"
],
"structure_id": "ysCnsCaq1pQwKUPP9H4AqE943C1XtLin3x6uCVN5Qh09IDyTg7Ey5A",
"thermostats": [
"G1jouHN5yl6mXFaQw5iGwXOu-iQr8PMV"
],
"time_zone": "America/Los_Angeles",
"wheres": {
"6UAWzz8czKpFrH6EK3AcjDiTjbRgts8x5MJxEnn1yKKQpYTBO7n2UQ": {
"name": "Downstairs Kitchen",
"where_id": "6UAWzz8czKpFrH6EK3AcjDiTjbRgts8x5MJxEnn1yKKQpYTBO7n2UQ"
},
"8tH6YiXUAQDZFLD6AgMmQ14Sc5wTG0NxKfabPY0XKrqc47t3uSDZvQ": {
"name": "Frog",
"where_id": "8tH6YiXUAQDZFLD6AgMmQ14Sc5wTG0NxKfabPY0XKrqc47t3uSDZvQ"
},
"qpWvTu89Knhn6GRFM-VtGoE4KYwbzbJg9INR6WyPfhW1EJ04GRyYbQ": {
"name": "Garage",
"where_id": "qpWvTu89Knhn6GRFM-VtGoE4KYwbzbJg9INR6WyPfhW1EJ04GRyYbQ"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIAYVvcpN1cOA": {
"name": "Family Room",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIAYVvcpN1cOA"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIB2f05cPKRBA": {
"name": "Kitchen",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIB2f05cPKRBA"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIB7GULj0y7Rw": {
"name": "Hallway",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIB7GULj0y7Rw"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIYpqdaXnYjUg": {
"name": "Basement",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIYpqdaXnYjUg"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIbTUmML4Q6xA": {
"name": "Kids Room",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIbTUmML4Q6xA"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIebdVzhA62Iw": {
"name": "Master Bedroom",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIebdVzhA62Iw"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIm5E0NfJPeeg": {
"name": "Downstairs",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsIm5E0NfJPeeg"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsJv12iEHQ0hxA": {
"name": "Driveway",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsJv12iEHQ0hxA"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsJyRQEOtmKqkw": {
"name": "Den",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsJyRQEOtmKqkw"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsK-nCnEjccnMQ": {
"name": "Bedroom",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsK-nCnEjccnMQ"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsK2kdsXRP3IFg": {
"name": "Entryway",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsK2kdsXRP3IFg"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKCxvyZfxNpKA": {
"name": "Upstairs",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKCxvyZfxNpKA"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKQrCrjN0yXiw": {
"name": "Living Room",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKQrCrjN0yXiw"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKR8TWb9hTptQ": {
"name": "Outside",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKR8TWb9hTptQ"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKZphUIYeW39g": {
"name": "Dining Room",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKZphUIYeW39g"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKfexoqPTcUVA": {
"name": "Backyard",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKfexoqPTcUVA"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKtUyRb3je64Q": {
"name": "Office",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsKtUyRb3je64Q"
},
"z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsLRu9lIioI47g": {
"name": "Front Yard",
"where_id": "z8fK075vJJPPWnXxLx1m3GskRSZQ64iQydB59k-UPsLRu9lIioI47g"
}
},
"wwn_security_state": "ok"
}
}
}
}