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:
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"access_token": "access_token",
|
||||
"expires_in": 315360000
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"error": "blocked",
|
||||
"type": "https://developer.nest.com/documentation/cloud/error-messages#blocked",
|
||||
"message": "blocked",
|
||||
"instance": "bb514046-edc9-4bca-8239-f7a3cfb0925a"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"path": "/",
|
||||
"data": {
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user