diff --git a/bundles/org.openhab.binding.nestdeviceaccess/NOTICE b/bundles/org.openhab.binding.nestdeviceaccess/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.nestdeviceaccess/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.nestdeviceaccess/README.md b/bundles/org.openhab.binding.nestdeviceaccess/README.md new file mode 100644 index 0000000000000..66e01c2d84f50 --- /dev/null +++ b/bundles/org.openhab.binding.nestdeviceaccess/README.md @@ -0,0 +1,103 @@ +# NestDeviceAccess Binding + +This binding integrates Nest products through the [Google Smart Device Management (SDM) API](https://developers.google.com/home/smart-device-management). + +![Nest Logo](doc/logo-google-nest_480.png) + +_If possible, provide some resources like pictures, a YouTube video, etc. to give an impression of what can be done with this binding. You can place such resources into a `doc` folder next to this README.md._ + +## Supported Things + +The NestDeviceAccess Binding will support things allowd by the Google Smart Device Management (SDM) API. Currently +the binding implements the Thermostat and device traits for Nest products defined at the [SDM traits](https://developers.google.com/nest/device-access/traits). + + +Thermostat Trait - Currently supported (Tested against generation 2 and 3 Nest Thermostats) + +![Nest Thermostat](doc/nestthermostat.jpeg) + +Doorbell Trait - (Needs to be implemented) +Camera Trait - (Needs to be implemented) + + +## Discovery + +The NestDeviceAccess binding works through discovery by leveraging the Google SDM API to perform a devices trait call to get all devices allowed by the accessToken. The devices are then enumerated to identify the "Type" of device. If it is a Thermostat "Type" then the device will be added to the inbox. + +Once added to the inbox, the device can be added as a thing. The thing will import several default properties to allow communication with the SDM API. + +Note: You MUST configure the discovery service through the services/cfg folder.. The format is listed under the BINDING CONFIGURATION section of this document. The file must be named nestdeviceaccess.cfg +## Binding Configuration + +'# Configuration for the Nest Device Access Binding +# +'# There is general project information for Google that must be provided in order to discover Nest products +'# The configuration data is a per project configuration and can be changed by the user. +'# A sandbox project created by Brian Higginbotham @BHigg was created and listed below for testing purposes only +'# Use the project at your own risk for testing or create your own project through Google and enable the SDM APIs for individual use +'# Note this project is limited in nature as a sandbox project to 30 API calls/min by Google. +'# +'# +'#projectId is the Google project provided through the project creation process +projectId= + +'#clientId is the Google clientId for your application +clientId= + +'#clientSecret is the Google clientSecret used to fetch the initial and refresh accessTokens +clientSecret= + +'#authorizationToken is used to authorize your devices with the project and provide the user with a unique refresh and first time access token. +authorizationToken= + +'#refreshToken is used to get accessTokens from the application +refreshToken= + +## Thing Configuration + +refreshInterval is used to tell the thing to refresh status (in seconds) and is required. + +## Channels + + +| channel | type | description | +|------------------|--------|-------------------------------------| +| thermostatName | Text | This is the name of the Thermostat | +| thermostatHumidtyPercent | Number:Length | This is the Humidity Percentage | +| thermostatAmbientTemperature | Number:Dimensionless | This is the ambient Temperature | +| thermostatTemperatureCool | Number:Dimensionless | This is the Cool Temperature Reading for the Thermostat (Only valid for Cool and Heat-Cool)| +| thermostatTemperatureHeat | Number:Dimensionless | This is the Heat Temperature Reading for the Thermostat (Only valid for Heat-Cool and Heat)| +| thermostatCurrentMode | Text | This is the current mode of the HVAC +| thermostatCurrentEcoMode | Text | This is the current mode of the Eco Setting for HVAC +| thermostatTargetTemperature | Number:Dimensionless | This is a aggregate temperature setting for the thermostat (Only valid for Heat and Cool) +| thermostatMinTemperature | Number:Dimensionless | This is a setting used for Eco and Heat-Cool HVAC Mode +| thermostatMaxTemperature | Number:Dimensionless | This is a setting used for Eco and Heat-Cool HVAC Mode +| thermostatScaleSetting | Text | This is the Scale setting for the Thermostat (FAHERNHEIT or CELSIUS) + + +## Full Example + +#Demo.sitemap +Frame label="Dining Room Thermostat" icon="temperature"{ + Switch item=NestDiningRoomThermostat_CurrentMode label="HVAC Mode" mappings=[OFF="OFF",COOL="COOL",HEAT="HEAT",HEATCOOL="HEATCOOL"] icon="climate" + Text item=NestDiningRoomThermostat_AmbientTemperature label="Current Ambient Temperature" icon="temperature" + Text item=NestDiningRoomThermostat_HumidityPercentage label="Current Humidity" icon="humidity" + Setpoint item=NestDiningRoomThermostat_TargetTemperatureSetting label="Target Temperature [%d]" minValue=65 maxValue=80 step=1 visibility=[NestDiningRoomThermostat_CurrentMode=="COOL",NestDiningRoomThermostat_ScaleSetting=="FAHRENHEIT"] +} + + + +## Any custom content here! + +The NestDeviceAccess Binding is built with a sandbox project in Google. This means that there is a limit of 30 requests to the API/min. This is used for testing. However, if you switch the services/nestdeviceaccess.cfg configuration for the binding to use a different projectId and clientId, you can use this binding on a better project. + +To configure the discovery service, you must place a nestdeviceaccess.cfg in the services dir + +You only need either the authorizationToken or refreshToken. If you use the authorizationToken, the binding will fetch your refreshToken and add it to the openhab log file (Make sure you update the nestdeviceaccess.cfg file with your refreshToken.) Otherwise, go through the linked instructions below and get your refreshToken and update the nestdeviceaccess.cfg file. + +It is pretty easy to see if the nest discovery works, if the parameters are in the nestdeviceaccess.cfg file, when you go to the inbox and try to add a NestDeviceAccess thing, it will start the discovery. Otherwise, it will ask for the parameters manually. + +Make sure you follow the instructions on [Google Nest Authorization instructions](https://developers.google.com/nest/device-access/authorize) in order to get your initial Authorization and Refresh token. You can store those in the nestdeviceaccess.cfg file for configuration of the discovery service. + +I've included a sample project projectId, clientId, and clientSecret in the nestdeviceaccess.cfg for testing purposes only. You can get the authorizationToken per the above instructions and I will output your refreshToken and initial accessToken in the openhab.log file. You will need to update the nestdeviceaccess.cfg file with this data after initial usage. + diff --git a/bundles/org.openhab.binding.nestdeviceaccess/cfg/nestdeviceaccess.cfg b/bundles/org.openhab.binding.nestdeviceaccess/cfg/nestdeviceaccess.cfg new file mode 100644 index 0000000000000..f271cfda6908c --- /dev/null +++ b/bundles/org.openhab.binding.nestdeviceaccess/cfg/nestdeviceaccess.cfg @@ -0,0 +1,23 @@ +# Configuration for the Nest Device Access Binding +# +# There is general project information for Google that must be provided in order to discover Nest products +# The configuration data is a per project configuration and can be changed by the user. +# A sandbox project created by Brian Higginbotham @BHigg was created and listed below for testing purposes only +# Use the project at your own risk for testing or create your own project through Google and enable the SDM APIs for individual use +# Note this project is limited in nature,a sandbox project, to 30 API calls/min by Google. +# +# +#projectId is the Google project provided through the project creation process +projectId=dbc4bd5a-7c1a-4905-abfb-b7e8ea792de7 + +#clientId is the Google clientId for your application +clientId=24189767352-9covhiu5jaiohte4idq9347789sh3g2m.apps.googleusercontent.com + +#clientSecreat is the Google clientSecret used to fetch the initial and refresh accessTokens +clientSecret=lr6PJbnqN2c_fRRVaqd_Fums + +#authorizationToken is used to authorize your devices with the project and provide the user with a unique refresh and first time access token. +authorizationToken= + +#refreshToken is used to get accessTokens from the application +refreshToken= \ No newline at end of file diff --git a/bundles/org.openhab.binding.nestdeviceaccess/doc/logo-google-nest_480.png b/bundles/org.openhab.binding.nestdeviceaccess/doc/logo-google-nest_480.png new file mode 100644 index 0000000000000..1a6356138e9a4 Binary files /dev/null and b/bundles/org.openhab.binding.nestdeviceaccess/doc/logo-google-nest_480.png differ diff --git a/bundles/org.openhab.binding.nestdeviceaccess/doc/nestthermostat.jpeg b/bundles/org.openhab.binding.nestdeviceaccess/doc/nestthermostat.jpeg new file mode 100644 index 0000000000000..a398312e514b9 Binary files /dev/null and b/bundles/org.openhab.binding.nestdeviceaccess/doc/nestthermostat.jpeg differ diff --git a/bundles/org.openhab.binding.nestdeviceaccess/pom.xml b/bundles/org.openhab.binding.nestdeviceaccess/pom.xml new file mode 100644 index 0000000000000..9aa513984e72c --- /dev/null +++ b/bundles/org.openhab.binding.nestdeviceaccess/pom.xml @@ -0,0 +1,269 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.0.0-SNAPSHOT + + + org.openhab.binding.nestdeviceaccess + + openHAB Add-ons :: Bundles :: nestdeviceaccess Binding + + + com.sun.jndi.dns;resolution:=optional,com.sun.nio.sctp;resolution:=optional,io.grpc.internal;resolution:=optional,android.net.*;resolution:=optional,com.squareup.okhttp.*;resolution:=optional,io.grpc.util;resolution:=optional,com.barchart.udt;resolution:=optional,com.fasterxml.aalto.*;resolution:=optional,com.oracle.svm.core.annotate;resolution:=optional,gnu.io.*;resolution:=optional,io.grpc.override;resolution:=optional,io.perfmark.*;resolution:=optional,lzma.sdk.*;resolution:=optional,net.jpountz.lz4;resolution:=optional,net.jpountz.xxhash;resolution:=optional,okio.*;resolution:=optional,org.bouncycastle.*;resolution:=optional,org.conscrypt.*;resolution:=optional,org.eclipse.jetty.*;resolution:=optional,org.jboss.marshalling;resolution:=optional,reactor.blockhound.*;resolution:=optional,sun.net.dns;resolution:=optional,sun.security.ssl;resolution:=optional,sun.security.x509;resolution:=optional,sun.security.util;resolution:=optional + + + + + com.google.api-client + google-api-client + 1.30.11 + + + com.google.oauth-client + google-oauth-client + 1.31.1 + + + com.google.api-client + google-api-client-jackson2 + 1.30.10 + + + com.google.api-client + google-api-client-servlet + 1.30.11 + + + com.fasterxml.jackson.core + jackson-core + 2.10.0 + + + com.google.protobuf.nano + protobuf-javanano + 3.1.0 + + + com.google.oauth-client + google-oauth-client-servlet + 1.31.1 + + + com.google.http-client + google-http-client + 1.30.1 + + + com.google.http-client + google-http-client-jackson2 + 1.36.0 + + + com.google.cloud + google-cloud-pubsub + 1.108.6 + + + com.google.api.grpc + proto-google-cloud-pubsub-v1 + 1.90.6 + + + com.google.api.grpc + proto-google-common-protos + 2.0.0 + + + io.grpc + grpc-alts + 1.33.0 + + + io.grpc + grpc-auth + 1.33.0 + + + io.grpc + grpc-grpclb + 1.33.0 + + + io.grpc + grpc-netty-shaded + 1.33.0 + + + io.grpc + grpc-protobuf + 1.33.0 + + + io.grpc + grpc-protobuf-lite + 1.33.0 + + + io.grpc + grpc-stub + 1.33.0 + + + org.apache.commons + commons-lang3 + 3.11 + + + com.google.api + api-common + 1.10.1 + + + com.google.api + gax + 1.60.0 + + + com.google.api + gax-grpc + 1.60.0 + + + com.google.auth + google-auth-library-credentials + 0.22.0 + + + com.google.auth + google-auth-library-oauth2-http + 0.22.0 + + + com.google.api.grpc + proto-google-iam-v1 + 1.0.2 + + + com.google.code.gson + gson + 2.8.5 + + + com.google.code.findbugs + jsr305 + 3.0.2 + + + org.json + json + 20200518 + + + io.opencensus + opencensus-api + 0.28.2 + + + io.opencensus + opencensus-contrib-http-util + 0.28.2 + + + io.grpc + grpc-all + 1.33.0 + + + io.grpc + grpc-api + 1.33.0 + + + com.sun.jndi + dns + 1.2 + pom + + + com.google.protobuf + protobuf-java + 3.13.0 + + + com.google.protobuf + protobuf-java-util + 3.13.0 + + + io.grpc + grpc-netty + 1.33.0 + + + io.grpc + grpc-okhttp + 1.33.0 + + + io.grpc + grpc-context + 1.33.0 + + + io.netty + netty-all + 4.1.53.Final + + + com.barchart.udt + barchart-udt-bundle + 2.3.0 + + + com.ning + compress-lzf + 1.0.4 + + + com.jcraft + jzlib + 1.1.3 + + + org.datanucleus + javax.jdo + 3.2.0-m13 + + + org.apache.httpcomponents + httpcore + 4.4.13 + + + org.apache.httpcomponents + httpclient + 4.5.13 + + + commons-codec + commons-codec + 1.15 + + + org.threeten + threetenbp + 1.5.0 + + + io.netty + netty-tcnative + 2.0.34.Final + + + diff --git a/bundles/org.openhab.binding.nestdeviceaccess/src/main/feature/feature.xml b/bundles/org.openhab.binding.nestdeviceaccess/src/main/feature/feature.xml new file mode 100644 index 0000000000000..5cb7edc03f977 --- /dev/null +++ b/bundles/org.openhab.binding.nestdeviceaccess/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.nestdeviceaccess/${project.version} + + diff --git a/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/discovery/nestdeviceaccessDiscovery.java b/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/discovery/nestdeviceaccessDiscovery.java new file mode 100644 index 0000000000000..ae4f91b0c85fd --- /dev/null +++ b/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/discovery/nestdeviceaccessDiscovery.java @@ -0,0 +1,236 @@ +/** + * 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.nestdeviceaccess.internal.discovery; + +import static org.openhab.binding.nestdeviceaccess.internal.nestdeviceaccessBindingConstants.*; + +import java.io.IOException; +import java.util.Collections; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.openhab.binding.nestdeviceaccess.internal.nesthelper.NestUtility; +import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.config.discovery.DiscoveryService; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.auth.oauth2.AccessToken; + +/** + * The {@link nestdeviceaccessDiscovery} is discovering devices from the SDM API + * + * @author Brian Higginbotham - Initial contribution + */ +@Component(service = DiscoveryService.class, immediate = true, configurationPid = "org.openhab.nestdeviceaccess") +public class nestdeviceaccessDiscovery extends AbstractDiscoveryService { + + NestUtility nestUtility; + private final Logger logger = LoggerFactory.getLogger(nestdeviceaccessDiscovery.class); + private ExecutorService executorService; + private boolean scanning; + private static final int TIMEOUT = 1000; + + private String projectId; + private String clientSecret; + private String clientId; + private String refreshToken; + private String authorizationToken; + private AccessToken googleAccessToken; + private long accessTokenExpiresIn; + private boolean initialize; + + @Activate + protected void activate(ComponentContext context) { + Dictionary properties = context.getProperties(); + + projectId = properties.get("projectId") == null ? null : properties.get("projectId").toString(); + clientSecret = properties.get("clientSecret") == null ? null : properties.get("clientSecret").toString(); + clientId = properties.get("clientId") == null ? null : properties.get("clientId").toString(); + refreshToken = properties.get("refreshToken") == null ? null : properties.get("refreshToken").toString(); + authorizationToken = properties.get("authorizationToken") == null ? null + : properties.get("authorizationToken").toString(); + + if (((projectId == null) || (clientId == null) || (clientSecret == null) || (refreshToken == null)) + && (authorizationToken == null)) { + logger.debug( + "activate in the Discovery service does NOT enough data to initialize.. projectId {}\nclientId {}\nclientSecret{}\nrefreshToken {}\nauthorizationToken {}", + projectId, clientId, clientSecret, refreshToken, authorizationToken); + initialize = false; + } else { + initialize = true; + } + } + + private void addThing(String devicesType[], String devicesId[], String devicesName[], String devicesStatus[]) { + + ThingTypeUID typeId = null; + + // Let's check type + for (int i = 0; i < devicesType.length; i++) { + switch (devicesType[i]) { + case "sdm.devices.types.THERMOSTAT": + if (devicesStatus[i].equalsIgnoreCase("online")) { + typeId = THING_TYPE_THERMOSTAT; + ThingUID deviceThing = new ThingUID(typeId, devicesId[i]); + Map properties = new HashMap<>(9); + properties.put("deviceId", devicesId[i]); + properties.put("deviceName", devicesName[i]); + properties.put("refreshToken", refreshToken); + properties.put("clientId", clientId); + properties.put("clientSecret", clientSecret); + properties.put("accessToken", googleAccessToken.getTokenValue()); + properties.put("accessTokenExpiration", googleAccessToken.getExpirationTime()); + properties.put("projectId", projectId); + DiscoveryResult result = DiscoveryResultBuilder.create(deviceThing).withProperties(properties) + .withLabel("Nest " + devicesName[i] + " Thermostat").build(); + thingDiscovered(result); + logger.info("nestdeviceaccessDiscovery adding Thermostat: [{}] to inbox", devicesName[i]); + break; + } + case "sdm.devices.types.DOORBELL": + typeId = THING_TYPE_DOORBELL; + ThingUID deviceThing = new ThingUID(typeId, devicesId[i]); + Map properties = new HashMap<>(9); + properties.put("deviceId", devicesId[i]); + properties.put("deviceName", devicesName[i]); + properties.put("refreshToken", refreshToken); + properties.put("clientId", clientId); + properties.put("clientSecret", clientSecret); + properties.put("accessToken", googleAccessToken.getTokenValue()); + properties.put("accessTokenExpiration", googleAccessToken.getExpirationTime()); + properties.put("projectId", projectId); + DiscoveryResult result = DiscoveryResultBuilder.create(deviceThing).withProperties(properties) + .withLabel("Nest " + devicesName[i] + " Doorbell").build(); + thingDiscovered(result); + logger.info("nestdeviceaccessDiscovery adding Doorbell: [{}] to inbox", devicesName[i]); + break; + } + } + } + + /** + * {@inheritDoc} + * + * Starts the scan. This discovery will: + * + */ + @Override + protected void startScan() { + + if (executorService != null) { + stopScan(); + } + scanning = true; + logger.debug("Starting Discovery NestDeviceAccess"); + + try { + + nestUtility = new NestUtility(projectId, clientId, clientSecret, refreshToken, googleAccessToken); + if ((!authorizationToken.equals("")) && (refreshToken.equals(""))) { + // initial authorization request. We need to get a refresh token + logger.debug("Initial Access Token being retrieved..."); + String[] tokens = new String[2]; + tokens = nestUtility.requestAccessToken(clientId, clientSecret, authorizationToken); + + } else { + // accessToken is typically stale.. Getting fresh on initialization + googleAccessToken = nestUtility.refreshAccessToken(refreshToken, clientId, clientSecret); + } + String url = "https://smartdevicemanagement.googleapis.com/v1/enterprises/" + projectId + "/devices"; + // get devices for discovery + String jsonContent = nestUtility.getDeviceInfo(url); + + JSONObject jo = new JSONObject(jsonContent); + JSONArray ja = jo.getJSONArray("devices"); + + String[][] devicesParentRelations = new String[ja.length()][2]; + String[] devicesType = new String[ja.length()]; + String[] devicesName = new String[ja.length()]; + String[] devicesId = new String[ja.length()]; + String[] devicesStatus = new String[ja.length()]; + + for (int i = 0; i < ja.length(); i++) { + devicesName[i] = ja.getJSONObject(i).getString("name"); + devicesId[i] = devicesName[i].substring(devicesName[i].lastIndexOf("/") + 1, devicesName[i].length()); + devicesType[i] = ja.getJSONObject(i).getString("type"); + if (ja.getJSONObject(i).getJSONObject("traits").has("sdm.devices.traits.Connectivity")) { + devicesStatus[i] = ja.getJSONObject(i).getJSONObject("traits") + .getJSONObject("sdm.devices.traits.Connectivity").getString("status"); + } + JSONArray jaParentRelations = ja.getJSONObject(i).getJSONArray("parentRelations"); + + for (int nCount = 0; nCount < jaParentRelations.length(); nCount++) { + // get Available Modes + devicesParentRelations[i][0] = jaParentRelations.getJSONObject(nCount).getString("parent"); + devicesParentRelations[i][1] = jaParentRelations.getJSONObject(nCount).getString("displayName"); + break; + } + devicesName[i] = devicesParentRelations[i][1]; // DisplayName + } + + addThing(devicesType, devicesId, devicesName, devicesStatus); + + } catch (IOException e) { + logger.debug("discovery reporting exception {}", e.getMessage()); + } + } + + /** + * {@inheritDoc} + * + * Stops the discovery scan. We set {@link #scanning} to false (allowing the listening threads to end naturally + * within {@link #TIMEOUT) * 5 time then shutdown the {@link #executorService} + */ + @Override + protected synchronized void stopScan() { + super.stopScan(); + /* + * if (executorService == null) { + * return; + * } + * + * scanning = false; + * + * try { + * executorService.awaitTermination(TIMEOUT * 5, TimeUnit.MILLISECONDS); + * } catch (InterruptedException e) { + * } + * executorService.shutdown(); + * executorService = null; + */ + } + + /** + * Constructs the discovery class using the thing IDs that we can discover. + */ + public nestdeviceaccessDiscovery() { + super(Collections.unmodifiableSet( + Stream.of(THING_TYPE_GENERIC, THING_TYPE_THERMOSTAT, THING_TYPE_DOORBELL).collect(Collectors.toSet())), + 30, false); + logger.debug("nestdeviceaccessDiscovery constructor.."); + } +} diff --git a/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/doorbell/NestDoorbell.java b/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/doorbell/NestDoorbell.java new file mode 100644 index 0000000000000..da6d0c24e3415 --- /dev/null +++ b/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/doorbell/NestDoorbell.java @@ -0,0 +1,183 @@ +/** + * 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.nestdeviceaccess.internal.doorbell; + +import java.io.IOException; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.openhab.binding.nestdeviceaccess.internal.nesthelper.NestUtility; +import org.openhab.core.thing.Thing; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link NestDoorbell} is responsible for handling all attributes and features of a Nest Doorbell + * + * @author Brian Higginbotham - Initial contribution + */ +public class NestDoorbell { + + public NestDoorbell(Thing thing) { + if (thing != null) { + this.thing = thing; + nestUtility = new NestUtility(this.thing); + } + } + + Thing thing; + NestUtility nestUtility; + + private final Logger logger = LoggerFactory.getLogger(NestDoorbell.class); + + // Thermostat properties + public String deviceName; + public String deviceType; + public String deviceCustomName; + public String deviceStatus; + public int[] deviceMaxImageResolution; + public int[] deviceMaxVideoResolution; + public String[] deviceVideoResolution; + public String[] deviceParentRelations; + public String[] deviceVideoCodecs; + public String[] deviceAudioCodecs; + public String[] deviceSupportedProtocols; + + public boolean parseDoorbellInfo(String jsonContent) { + JSONObject jo = new JSONObject(jsonContent); + + deviceParentRelations = new String[2]; + deviceType = jo.getString("type"); + + if (jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.Info").has("customName")) { + deviceCustomName = jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.Info") + .getString("customName"); + } + + if (jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.CameraLiveStream").has("maxVideoResolution")) { + deviceMaxVideoResolution = new int[2]; + + deviceMaxVideoResolution[0] = jo.getJSONObject("traits") + .getJSONObject("sdm.devices.traits.CameraLiveStream").getJSONObject("maxVideoResolution") + .getInt("width"); + deviceMaxVideoResolution[1] = jo.getJSONObject("traits") + .getJSONObject("sdm.devices.traits.CameraLiveStream").getJSONObject("maxVideoResolution") + .getInt("height"); + } + + if (jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.CameraLiveStream").has("videoCodecs")) { + JSONArray jaVideoCodecs = jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.CameraLiveStream") + .getJSONArray("videoCodecs"); + deviceVideoCodecs = new String[jaVideoCodecs.length()]; + for (int nCount = 0; nCount < jaVideoCodecs.length(); nCount++) { + deviceVideoCodecs[nCount] = jaVideoCodecs.getString(nCount); + } + } + + if (jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.CameraLiveStream").has("audioCodecs")) { + JSONArray jaAudioCodecs = jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.CameraLiveStream") + .getJSONArray("audioCodecs"); + deviceAudioCodecs = new String[jaAudioCodecs.length()]; + for (int nCount = 0; nCount < jaAudioCodecs.length(); nCount++) { + deviceVideoCodecs[nCount] = jaAudioCodecs.getString(nCount); + } + } + + if (jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.CameraLiveStream").has("supportedProtocols")) { + JSONArray jaSupportedProtocols = jo.getJSONObject("traits") + .getJSONObject("sdm.devices.traits.CameraLiveStream").getJSONArray("supportedProtocols"); + deviceSupportedProtocols = new String[jaSupportedProtocols.length()]; + for (int nCount = 0; nCount < jaSupportedProtocols.length(); nCount++) { + deviceVideoCodecs[nCount] = jaSupportedProtocols.getString(nCount); + } + } + + if (jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.CameraLiveStream").has("maxImageResolution")) { + deviceMaxImageResolution = new int[2]; + deviceMaxImageResolution[0] = jo.getJSONObject("traits") + .getJSONObject("sdm.devices.traits.CameraLiveStream").getJSONObject("maxImageResolution") + .getInt("width"); + deviceMaxImageResolution[1] = jo.getJSONObject("traits") + .getJSONObject("sdm.devices.traits.CameraLiveStream").getJSONObject("maxImageResolution") + .getInt("height"); + } + + JSONArray jaParentRelations = jo.getJSONArray("parentRelations"); + + for (int nCount = 0; nCount < jaParentRelations.length(); nCount++) { + // get Available Modes + deviceParentRelations[0] = jaParentRelations.getJSONObject(nCount).getString("parent"); + deviceParentRelations[1] = jaParentRelations.getJSONObject(nCount).getString("displayName"); + break; + } + deviceName = deviceParentRelations[1]; + + return (true); + } + + public boolean getDevices() throws IOException { + try { + String url = "https://smartdevicemanagement.googleapis.com/v1/enterprises/" + + thing.getProperties().get("projectId") + "/devices"; + nestUtility.getDeviceInfo(url); + return (true); + } catch (IOException e) { + throw new IOException(e.getMessage()); + } + } + + public boolean initializeDoorbell() throws IOException { + return (getDevices()); + } + + public int[] getMaxImageResolution() { + return (deviceMaxImageResolution); + } + + public String[] getVideoCodecs() { + return (deviceVideoCodecs); + } + + public String[] getAudioCodecs() { + return (deviceAudioCodecs); + } + + public String[] getSupportedProtocols() { + return (deviceSupportedProtocols); + } + + public int[] getMaxVideoResolution() { + return (deviceMaxVideoResolution); + } + + public String getDeviceName() { + return (deviceName); + } + + public String getCustomName() { + return (deviceCustomName); + } + + public boolean getDoorbellInfo() throws IOException { + try { + String jsonContent; + jsonContent = nestUtility.getDeviceInfo("https://smartdevicemanagement.googleapis.com/v1/enterprises/" + + thing.getProperties().get("projectId") + "/devices/" + thing.getProperties().get("deviceId")); + + return (parseDoorbellInfo(jsonContent)); + + } catch (IOException e) { + throw new IOException(e.getMessage()); + } + } +} diff --git a/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/doorbell/NestDoorbellHandler.java b/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/doorbell/NestDoorbellHandler.java new file mode 100644 index 0000000000000..9cf9281e5546e --- /dev/null +++ b/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/doorbell/NestDoorbellHandler.java @@ -0,0 +1,187 @@ +/** + * 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.nestdeviceaccess.internal.doorbell; + +import java.io.IOException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nestdeviceaccess.internal.nestdeviceaccessConfiguration; +import org.openhab.binding.nestdeviceaccess.internal.nesthelper.NestUtility; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link NestDoorbellHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Brian Higginbotham - Initial contribution + */ +@NonNullByDefault +public class NestDoorbellHandler extends BaseThingHandler { + + NestUtility nestUtility = new NestUtility(thing); + private final Logger logger = LoggerFactory.getLogger(NestDoorbellHandler.class); + + private @NonNullByDefault({}) ScheduledFuture refreshJob; + private @Nullable nestdeviceaccessConfiguration config; + NestDoorbell nestDoorbell; + + public NestDoorbellHandler(Thing thing) { + super(thing); + nestDoorbell = new NestDoorbell(thing); // initialize Doorbell with base properties + } + + @Override + public void dispose() { + if (refreshJob != null) { + refreshJob.cancel(true); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.debug("handleCommand reporting {},{}", channelUID.getId(), command.toString()); + /* + * try { + * + * if (doorbellName.equals(channelUID.getId())) { + * + * if (command instanceof RefreshType) { + * // TODO: handle data refresh + * + * logger.debug("handleCommand reporting {}", command.toString()); + * } + * + * // TODO: handle command + * + * // Note: if communication with thing fails for some reason, + * // indicate that by setting the status with detail information: + * // updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + * // "Could not control device at IP address x.x.x.x"); + * } + * } catch (IOException e) { + * logger.debug("handleMessage reporting exception {}", e.getMessage()); + * } catch (InterruptedException e) { + * // TODO Auto-generated catch block + * logger.debug("handleMessage reporting exception {}", e.getMessage()); + * } + */ + } + + void refreshChannels() { + logger.info("refreshChannels process a timer for Updating thing Channels for:{}", thing.getUID()); + + try { + // refresh status of the device + + if (nestDoorbell.getDoorbellInfo()) { + /* + * State statusState = new StringType(nestThermostat.getDeviceStatus()); + * updateState("thermostatStatus", statusState); + * State nameState = new StringType(nestThermostat.getDeviceName()); + * updateState("thermostatName", nameState); + * State humidityState = new DecimalType(nestThermostat.getCurrentHumidity()); + * updateState("thermostatHumidityPercent", humidityState); + * State ambientState = new DecimalType(nestThermostat.getAmbientTemperatureSetting()); + * updateState("thermostatAmbientTemperature", ambientState); + * State setTempHeatState = new DecimalType(nestThermostat.getCurrentTemperatureHeat()); + * updateState("thermostatTemperatureHeat", setTempHeatState); + * State setTempCoolState = new DecimalType(nestThermostat.getCurrentTemperatureCool()); + * updateState("thermostatTemperatureCool", setTempCoolState); + * State setTargetTempState = new DecimalType(nestThermostat.getTargetTemperature()); + * updateState("thermostatTargetTemperature", setTargetTempState); + * State setMinTempState = new DecimalType(nestThermostat.getMinMaxTemperature()[0]); + * updateState("thermostatMinimumTemperature", setMinTempState); + * State setMaxTempState = new DecimalType(nestThermostat.getMinMaxTemperature()[1]); + * updateState("thermostatMaximumTemperature", setMaxTempState); + * State modeState = new StringType(nestThermostat.getThermostatMode()); + * updateState("thermostatCurrentMode", modeState); + * State ecoModeState = new StringType(nestThermostat.getThermostatEcoMode()); + * updateState("thermostatCurrentEcoMode", ecoModeState); + * State scaleSettingState = new StringType(nestThermostat.getTemperatureScaleSetting()); + * updateState("thermostatScaleSetting", scaleSettingState); + */ + } + } catch (IOException e) { + logger.debug("refreshChannels() reporting exception {}", e.getMessage()); + } + } + + @Override + public void initialize() { + logger.debug("Start initializing!"); + config = getConfigAs(nestdeviceaccessConfiguration.class); + config.projectId = thing.getProperties().get("projectId"); + config.clientId = thing.getProperties().get("clientId"); + config.clientSecret = thing.getProperties().get("clientSecret"); + config.accessToken = thing.getProperties().get("accessToken"); + config.refreshToken = thing.getProperties().get("refreshToken"); + config.deviceId = thing.getProperties().get("deviceId"); + config.deviceName = thing.getProperties().get("deviceName"); + config.refreshInterval = Integer.parseInt(thing.getConfiguration().get("refreshInterval").toString()); + + // TODO: Initialize the handler. + // The framework requires you to return from this method quickly. Also, before leaving this method a thing + // status from one of ONLINE, OFFLINE or UNKNOWN must be set. This might already be the real thing status in + // case you can decide it directly. + // In case you can not decide the thing status directly (e.g. for long running connection handshake using WAN + // access or similar) you should set status UNKNOWN here and then decide the real status asynchronously in the + // background. + + // set the thing status to UNKNOWN temporarily and let the background task decide for the real status. + // the framework is then able to reuse the resources from the thing handler initialization. + // we set this upfront to reliably check status updates in unit tests. + + updateStatus(ThingStatus.UNKNOWN); + + // Example for background initialization: + scheduler.execute(() -> { + boolean thingReachable = true; // + try { + + nestDoorbell.initializeDoorbell(); + // NestUtility.pubSubEventHandler("openhab-nest-int-1601138253554", "sdm_pull_events"); + // NestUtility.listSubscriptionInProjectExample(config.projectId); + // when done do: + thingReachable = true; // No status on doorbell + if (thingReachable) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE); + } + + } catch (Exception e) { + logger.debug("Initialize() caught an exception {}", e.getMessage()); + } + }); + + if (refreshJob == null || refreshJob.isCancelled()) { + refreshJob = scheduler.scheduleWithFixedDelay(this::refreshChannels, 0, config.refreshInterval, + TimeUnit.SECONDS); + } + // Note: When initialization can NOT be done set the status with more details for further + // analysis. See also class ThingStatusDetail for all available status details. + // Add a description to give user information to understand why thing does not work as expected. E.g. + // updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + // "Can not access device as username and/or password are invalid"); + } +} diff --git a/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/nestdeviceaccessBindingConstants.java b/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/nestdeviceaccessBindingConstants.java new file mode 100644 index 0000000000000..da7fb564d0598 --- /dev/null +++ b/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/nestdeviceaccessBindingConstants.java @@ -0,0 +1,42 @@ +/** + * 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.nestdeviceaccess.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link nestdeviceaccessBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Brian Higginbotham - Initial contribution + */ +@NonNullByDefault +public class nestdeviceaccessBindingConstants { + + private static final String BINDING_ID = "nestdeviceaccess"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_GENERIC = new ThingTypeUID(BINDING_ID, "nest-device-access"); + public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "nest-device-thermostat"); + public static final ThingTypeUID THING_TYPE_DOORBELL = new ThingTypeUID(BINDING_ID, "nest-device-doorbell"); + // List of all Channel ids + public static final String thermostatName = "thermostatName"; + public static final String thermostatCurrentMode = "thermostatCurrentMode"; + public static final String thermostatCurrentEcoMode = "thermostatCurrentEcoMode"; + public static final String thermostatTargetTemperature = "thermostatTargetTemperature"; + public static final String thermostatMinimumTemperature = "thermostatMinimumTemperature"; + public static final String thermostatMaximumTemperature = "thermostatMaximumTemperature"; + public static final String thermostatScaleSetting = "thermostatScaleSetting"; + public static final String doorbellName = "doorbellName"; +} diff --git a/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/nestdeviceaccessConfiguration.java b/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/nestdeviceaccessConfiguration.java new file mode 100644 index 0000000000000..4eaea6d9e01db --- /dev/null +++ b/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/nestdeviceaccessConfiguration.java @@ -0,0 +1,35 @@ +/** + * 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.nestdeviceaccess.internal; + +/** + * The {@link nestdeviceaccessConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Brian Higginbotham - Initial contribution + */ +public class nestdeviceaccessConfiguration { + + /** + * Sample configuration parameter. Replace with your own. + */ + public String projectId; + public String clientId; + public String clientSecret; + public String authorizationToken; + public String accessToken; + public String refreshToken; + public String accessTokenExpiresIn; + public String deviceId; + public String deviceName; + public int refreshInterval; +} diff --git a/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/nestdeviceaccessHandler.java b/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/nestdeviceaccessHandler.java new file mode 100644 index 0000000000000..3de19347fa936 --- /dev/null +++ b/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/nestdeviceaccessHandler.java @@ -0,0 +1,150 @@ +/** + * 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.nestdeviceaccess.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nestdeviceaccess.internal.nesthelper.NestUtility; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link nestdeviceaccessHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Brian Higginbotham - Initial contribution + */ +@NonNullByDefault +public class nestdeviceaccessHandler extends BaseThingHandler { + + NestUtility nestUtility = new NestUtility(thing); + private final Logger logger = LoggerFactory.getLogger(nestdeviceaccessHandler.class); + + private @Nullable nestdeviceaccessConfiguration config; + + public nestdeviceaccessHandler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (nestdeviceaccessBindingConstants.thermostatName.equals(channelUID.getId())) { + if (command instanceof RefreshType) { + // TODO: handle data refresh + } + + // TODO: handle command + + // Note: if communication with thing fails for some reason, + // indicate that by setting the status with detail information: + // updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + // "Could not control device at IP address x.x.x.x"); + } + } + + @Override + public void initialize() { + logger.debug("Start initializing!"); + config = getConfigAs(nestdeviceaccessConfiguration.class); + config.projectId = thing.getConfiguration().get("projectId").toString(); + config.clientId = thing.getConfiguration().get("clientId").toString(); + config.clientSecret = thing.getConfiguration().get("clientSecret").toString(); + config.authorizationToken = thing.getConfiguration().get("authorizationToken").toString(); + config.accessToken = thing.getConfiguration().get("accessToken").toString(); + config.refreshToken = thing.getConfiguration().get("refreshToken").toString(); + config.accessTokenExpiresIn = thing.getConfiguration().get("accessTokenExpiresIn").toString(); + // verify accesstoken with simple get request + /* + * try { + * if (nestUtility.getStructures(config.projectId, config.accessToken) == 401) { + * // get access token refreshed + * logger.debug("initialize() reporting access token is expired.."); + * if (nestUtility.refreshAccessToken(config.refreshToken, config.clientId, config.clientSecret)) { + * logger.debug("initialize() reporting access token refresh successful.."); + * if (nestUtility.getStructures(config.projectId, config.accessToken) == 200) { + * config.accessToken = nestUtility.accessToken; + * config.accessTokenExpiresIn = nestUtility.accessTokenExpiresIn; + * logger.debug("initialize() reporting call to getStructures successful"); + * } + * } + * } + * + * } catch (IOException e) { + * logger.debug("initialize() {}", e.getMessage()); + * } + * + * if (thing.getConfiguration().get("accessToken") != null) { + * config.accessToken = thing.getConfiguration().get("accessToken").toString(); + * } + * if (thing.getConfiguration().get("refreshToken") != null) { + * config.refreshToken = thing.getConfiguration().get("refreshToken").toString(); + * } + */ + // TODO: Initialize the handler. + // The framework requires you to return from this method quickly. Also, before leaving this method a thing + // status from one of ONLINE, OFFLINE or UNKNOWN must be set. This might already be the real thing status in + // case you can decide it directly. + // In case you can not decide the thing status directly (e.g. for long running connection handshake using WAN + // access or similar) you should set status UNKNOWN here and then decide the real status asynchronously in the + // background. + + // set the thing status to UNKNOWN temporarily and let the background task decide for the real status. + // the framework is then able to reuse the resources from the thing handler initialization. + // we set this upfront to reliably check status updates in unit tests. + + updateStatus(ThingStatus.UNKNOWN); + + // Example for background initialization: + scheduler.execute(() -> { + boolean thingReachable = true; // + /* + * try { + * logger.debug("initialize executing..."); + * if ((config.accessToken == null) && (config.refreshToken == null)) { + * logger.debug("initialize getting access token..."); + * nestUtility.requestAccessToken(config.clientId, config.clientSecret, config.authorizationToken); + * } else { + * logger.info("Initialize reporting a preset access {} and refresh {} token..", config.accessToken, + * config.refreshToken); + * thing.getConfiguration().put("accessToken", config.accessToken); + * thing.getConfiguration().put("refreshToken", config.refreshToken); + * } + * + * } catch (IOException e) { + * logger.debug("Initialize() reporting {}", e.getMessage()); + * } + */ + // when done do: + if (thingReachable) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE); + } + + }); + + // logger.debug("Finished initializing!"); + + // Note: When initialization can NOT be done set the status with more details for further + // analysis. See also class ThingStatusDetail for all available status details. + // Add a description to give user information to understand why thing does not work as expected. E.g. + // updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + // "Can not access device as username and/or password are invalid"); + } +} diff --git a/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/nestdeviceaccessHandlerFactory.java b/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/nestdeviceaccessHandlerFactory.java new file mode 100644 index 0000000000000..33aac6c11a775 --- /dev/null +++ b/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/nestdeviceaccessHandlerFactory.java @@ -0,0 +1,87 @@ +/** + * 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 + */ +/** + * 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.nestdeviceaccess.internal; + +import static org.openhab.binding.nestdeviceaccess.internal.nestdeviceaccessBindingConstants.*; + +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nestdeviceaccess.internal.doorbell.NestDoorbellHandler; +import org.openhab.binding.nestdeviceaccess.internal.thermostat.NestThermostatHandler; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link nestdeviceaccessHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Brian Higginbotham - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.nestdeviceaccess", service = ThingHandlerFactory.class) +public class nestdeviceaccessHandlerFactory extends BaseThingHandlerFactory { + private final Logger logger = LoggerFactory.getLogger(nestdeviceaccessHandlerFactory.class); + + private static final Set SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet( + Stream.of(THING_TYPE_GENERIC, THING_TYPE_THERMOSTAT, THING_TYPE_DOORBELL).collect(Collectors.toSet())); + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + logger.info("supportsThingType reporting {}", thingTypeUID.toString()); + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + logger.info("createHandler reporting {}", thingTypeUID.toString()); + + if (thingTypeUID.equals(THING_TYPE_GENERIC)) { + logger.debug("createHandler reporting Generic.."); + return new nestdeviceaccessHandler(thing); + } + if (thingTypeUID.equals(THING_TYPE_THERMOSTAT)) { + logger.debug("createHandler reporting Thermostat.."); + return new NestThermostatHandler(thing); + } + if (thingTypeUID.equals(THING_TYPE_DOORBELL)) { + logger.debug("createHandler reporting Doorbell.."); + return new NestDoorbellHandler(thing); + } + logger.info("createHandler never should have come here.. Returning null"); + return null; + } +} diff --git a/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/nesthelper/NestUtility.java b/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/nesthelper/NestUtility.java new file mode 100644 index 0000000000000..7c6f4191c2321 --- /dev/null +++ b/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/nesthelper/NestUtility.java @@ -0,0 +1,324 @@ +/** + * 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.nestdeviceaccess.internal.nesthelper; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; + +import org.openhab.core.thing.Thing; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.api.client.auth.oauth2.AuthorizationCodeTokenRequest; +import com.google.api.client.auth.oauth2.TokenResponse; +import com.google.api.client.auth.oauth2.TokenResponseException; +import com.google.api.client.googleapis.auth.oauth2.GoogleRefreshTokenRequest; +import com.google.api.client.http.BasicAuthentication; +import com.google.api.client.http.ByteArrayContent; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.auth.oauth2.AccessToken; +import com.google.common.net.HttpHeaders; + +/** + * The {@link NestUtility} is general utility class to help with all Nest SDM APIs + * + * @author Brian Higginbotham - Initial contribution + */ +public class NestUtility { + + public NestUtility(Thing thing) { + if (thing != null) { + this.thing = thing; + deviceId = this.thing.getProperties().get("deviceId"); + clientId = this.thing.getProperties().get("clientId"); + clientSecret = this.thing.getProperties().get("clientSecret"); + projectId = this.thing.getProperties().get("projectId"); + refreshToken = this.thing.getProperties().get("refreshToken"); + accessToken = this.thing.getProperties().get("accessToken"); + accessTokenExpiration = this.thing.getProperties().get("accessTokenExpiration"); + + SimpleDateFormat format = new SimpleDateFormat("E MMM dd HH:mm:ss zzz yyyy"); + try { + if (accessTokenExpiration != null) { + Date date = format.parse(accessTokenExpiration); + googleAccessToken = new AccessToken(accessToken, date); + } else { + googleAccessToken = refreshAccessToken(refreshToken, clientId, clientSecret); + thing.setProperty("accessTokenExpiration", googleAccessToken.getExpirationTime().toString()); + } + } catch (ParseException e) { + logger.debug("NestUtility constructor failed to parse date {}", e.getMessage()); + } catch (IOException e) { + logger.debug("NestUtility constructor failed with exception {}", e.getMessage()); + } + } + } + + public NestUtility(String projectId, String clientId, String clientSecret, String refreshToken, + AccessToken accessToken) { + // secondary constructor for null thing and discovery service/non-basehandlers + if (thing == null) { + this.projectId = projectId; + this.clientId = clientId; + this.clientSecret = clientSecret; + this.refreshToken = refreshToken; + NestUtility.googleAccessToken = accessToken; + } + } + + Thing thing; + + private final static Logger logger = LoggerFactory.getLogger(NestUtility.class); + + private String deviceId; + private String clientId; + private String clientSecret; + private String projectId; + private String refreshToken; + private String accessToken; + private String accessTokenExpiration; + private long accessTokenExpiresIn; + private static AccessToken googleAccessToken; + + // helper local vars. Will set global properties file + + public AccessToken getAccessToken() throws IOException { + try { + if (isAccessTokenExpired()) { + // we need to refresh the access token + return (refreshAccessToken(refreshToken, clientId, clientSecret)); + } else { + return (googleAccessToken); + } + } catch (IOException e) { + throw new IOException(e.getMessage()); + } + } + + public AccessToken setAccessToken(String accessToken, int accessTokenExpiresIn) { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.SECOND, accessTokenExpiresIn); + googleAccessToken = new AccessToken(accessToken, calendar.getTime()); + return (googleAccessToken); + } + + public boolean isAccessTokenExpired() { + + if (googleAccessToken.getExpirationTime().compareTo(Calendar.getInstance().getTime()) < 0) { + return (true); + } else { + return (false); + } + } + + public String deviceExecuteCommand(String deviceId, String projectId, String accessToken, String requestBody) + throws IOException { + + try { + HttpTransport transport = new NetHttpTransport(); + HttpRequest request = transport.createRequestFactory().buildPostRequest( + new GenericUrl("https://smartdevicemanagement.googleapis.com/v1/enterprises/" + projectId + + "/devices/" + deviceId + ":executeCommand"), + ByteArrayContent.fromString("application/json", requestBody)); + request.getHeaders().set(HttpHeaders.CONTENT_TYPE, "application/json"); + request.getHeaders().set(HttpHeaders.AUTHORIZATION, "Bearer " + getAccessToken().getTokenValue()); + + HttpResponse response = request.execute(); + return (convertStreamtoString(response.getContent())); + } catch (IOException e) { + // int statusCode = Integer.parseInt(e.getMessage().substring(0, 3)); + /* + * if (statusCode == 401) { + * // get access token refreshed + * logger.debug("deviceExecuteCommand reporting access token is expired.."); + * refreshAccessToken(refreshToken, clientId, clientSecret); + * logger.debug("deviceExecuteCommand reporting access token refresh successful.."); + * return (deviceExecuteCommand(deviceId, projectId, getAccessToken().getTokenValue(), requestBody)); // + * returns + */ + // error + throw new IOException(e.getMessage()); + } + } + + public String getDeviceInfo(String url) throws IOException { + try { + HttpTransport transport = new NetHttpTransport(); + HttpRequest request = transport.createRequestFactory().buildGetRequest(new GenericUrl(url)); + + request.getHeaders().set(HttpHeaders.CONTENT_TYPE, "application/json"); + request.getHeaders().set(HttpHeaders.AUTHORIZATION, "Bearer " + getAccessToken().getTokenValue()); + HttpResponse response = request.execute(); + + return (convertStreamtoString(response.getContent())); + } catch (IOException e) { + /* + * int statusCode = Integer.parseInt(e.getMessage().substring(0, 3)); + * + * if (statusCode == 401) { + * // get access token refreshed + * logger.debug("deviceGetInfo reporting access token is expired.."); + * accessToken = refreshAccessToken(refreshToken, clientId, clientSecret); + * logger.debug("deviceGetInfo reporting access token refresh successful.."); + * + * return (getDeviceInfo(accessToken, url)); // returns error code in string + * // format. Did this to + * // commoditize the + * // return value for successful JSON response + */ + throw new IOException(e.getMessage()); // never should get here unless there is a problem + } + } + + public AccessToken refreshAccessToken(String refreshToken, String clientId, String clientSecret) + throws IOException { + try { + String accessToken; + TokenResponse response = new GoogleRefreshTokenRequest(new NetHttpTransport(), new JacksonFactory(), + refreshToken, clientId, clientSecret).execute(); + // logger.info("Access Token: {}", response.getAccessToken()); + // logger.info("Refresh Token Lifespan: {}", response.getExpiresInSeconds()); + accessToken = response.getAccessToken(); + accessTokenExpiresIn = response.getExpiresInSeconds(); + googleAccessToken = setAccessToken(accessToken, (int) accessTokenExpiresIn); + if (thing != null) { + thing.setProperty("accessTokenExpiresIn", response.getExpiresInSeconds().toString()); + thing.setProperty("accessToken", googleAccessToken.getTokenValue()); + } + return (googleAccessToken); + } catch (TokenResponseException e) { + logger.debug("refreshAccessToken threw an exception {}", e.getDetails().getError()); + if (e.getDetails().getErrorDescription() != null) { + logger.debug("refreshAccessToken threw further description {}", e.getDetails().getErrorDescription()); + } + throw new IOException(e.getMessage()); + } + } + + public String[] requestAccessToken(String clientId, String clientSecret, String authorizationToken) + throws IOException { + try { + String accessToken; + String[] tokens = new String[2]; + TokenResponse response = new AuthorizationCodeTokenRequest(new NetHttpTransport(), new JacksonFactory(), + new GenericUrl("https://www.googleapis.com/oauth2/v4/token"), authorizationToken) + .setRedirectUri("https://www.google.com") + .setClientAuthentication(new BasicAuthentication(clientId, clientSecret)).execute(); + // logger.info("Access Token: {}", response.getAccessToken()); + // logger.info("Refresh Token: {}", response.getRefreshToken()); + // logger.info("Refresh Token Lifespan: {}", response.getExpiresInSeconds()); + accessToken = response.getAccessToken(); + refreshToken = response.getRefreshToken(); + + accessTokenExpiresIn = response.getExpiresInSeconds(); + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.SECOND, (int) accessTokenExpiresIn); + + // Construct a proper AccessToken + googleAccessToken = new AccessToken(accessToken, calendar.getTime()); + + tokens[0] = accessToken; + tokens[1] = refreshToken; + if (thing != null) { + thing.setProperty("accessToken", response.getAccessToken()); + thing.setProperty("refreshToken", response.getRefreshToken()); + thing.setProperty("accessTokenExpiresIn", response.getExpiresInSeconds().toString()); + } + return (tokens); + } catch (TokenResponseException e) { + throw new IOException(e.getMessage()); + } + } + + /* Took function from online stackoverflow article... credit: Zapnologica */ + private String convertStreamtoString(InputStream is) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + StringBuilder sb = new StringBuilder(); + + String line = ""; + try { + while ((line = reader.readLine()) != null) { + sb.append(line + "\n"); + } + is.close(); + return sb.toString(); + } catch (IOException e) { + throw new IOException(e.getMessage()); + } + } + + public String getDevices(String projectId, AccessToken accessToken) throws IOException { + + try { + HttpTransport transport = new NetHttpTransport(); + HttpRequest request = transport.createRequestFactory().buildGetRequest(new GenericUrl( + "https://smartdevicemanagement.googleapis.com/v1/enterprises/" + projectId + "/devices")); + + request.getHeaders().set(HttpHeaders.CONTENT_TYPE, "application/json"); + request.getHeaders().set(HttpHeaders.AUTHORIZATION, "Bearer " + getAccessToken().getTokenValue()); + HttpResponse response = request.execute(); + + return (convertStreamtoString(response.getContent())); + + } catch (IOException e) { + logger.debug("getDevices returning exception {}", e.getMessage()); + return ((e.getMessage().substring(0, 3))); + } + } + + /* + * public int getStructures(String projectId, String accessToken) throws IOException { + * + * try { + * HttpTransport transport = new NetHttpTransport(); + * HttpRequest request = transport.createRequestFactory().buildGetRequest(new GenericUrl( + * "https://smartdevicemanagement.googleapis.com/v1/enterprises/" + projectId + "/structures")); + * + * request.getHeaders().set(HttpHeaders.CONTENT_TYPE, "application/json"); + * request.getHeaders().set(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken); + * HttpResponse response = request.execute(); + * + * JSONObject jo = new JSONObject(convertStreamtoString(response.getContent())); + * JSONArray ja = jo.getJSONArray("structures"); + * // allocate structures array + * structureId = new String[ja.length()]; + * structureName = new String[ja.length()]; + * + * for (int i = 0; i < ja.length(); i++) { + * structureName[i] = ja.getJSONObject(i).getJSONObject("traits") + * .getJSONObject("sdm.structures.traits.Info").getString("customName"); + * String temp = ja.getJSONObject(i).getString("name"); + * structureId[i] = temp.substring(temp.lastIndexOf("/") + 1, temp.length()); + * logger.debug("getStructures reporting id {} and name {}", structureId[i], structureName[i]); + * } + * + * response.getContent().close(); + * return (response.getStatusCode()); + * } catch (IOException e) { + * logger.debug("getStructures returning exception {}", e.getMessage()); + * return (Integer.parseInt((e.getMessage().substring(0, 3)))); + * } + * } + */ +} diff --git a/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/thermostat/NestThermostat.java b/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/thermostat/NestThermostat.java new file mode 100644 index 0000000000000..6300f70d3320a --- /dev/null +++ b/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/thermostat/NestThermostat.java @@ -0,0 +1,423 @@ +/** + * 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.nestdeviceaccess.internal.thermostat; + +import java.io.IOException; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.openhab.binding.nestdeviceaccess.internal.nesthelper.NestUtility; +import org.openhab.core.thing.Thing; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link NestThermostat} is responsible for handling all attributes and features of a Nest Thermostat + * + * @author Brian Higginbotham - Initial contribution + */ +public class NestThermostat { + + public NestThermostat(Thing thing) { + if (thing != null) { + this.thing = thing; + nestUtility = new NestUtility(this.thing); + } + } + + Thing thing; + NestUtility nestUtility; + + private final Logger logger = LoggerFactory.getLogger(NestThermostat.class); + + // Thermostat properties + public String deviceName; + public String deviceType; + public String deviceCustomName; + public int deviceHumidityPercent; + public String deviceStatus; + public String deviceFan; + public String deviceCurrentThermostatMode; + public String[] deviceAvailableThermostatModes; + public String deviceThermostatEcoMode; + public String[] deviceAvailableThermostatEcoModes; + public double deviceCurrentThermostatEcoHeatCelsius; + public double deviceCurrentThermostatEcoCoolCelsius; + public double deviceCurrentThermostatHeatCelsius; + public double deviceCurrentThermostatCoolCelsius; + public String deviceThermostatHVACStatus; + public String deviceTemperatureScaleSetting; + public double deviceAmbientTemperatureSetting; + public double deviceTargetTemperature; // settings used to aggregate target setting on heat/cool + public double deviceMinTemperature; // settings used to aggregate setting for eco and heat-cool + public double deviceMaxTemperature; // settings used to aggregate setting for eco and heat-cool + public String[] deviceParentRelations; + + public boolean parseThermostatInfo(String jsonContent) { + JSONObject jo = new JSONObject(jsonContent); + + deviceParentRelations = new String[2]; + deviceAvailableThermostatEcoModes = new String[2]; // only two known eco modes + deviceAvailableThermostatModes = new String[4]; + + deviceType = jo.getString("type"); + + if (jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.Info").has("customName")) { + deviceCustomName = jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.Info") + .getString("customName"); + } + + if (jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.Humidity").has("ambientHumidityPercent")) { + deviceHumidityPercent = jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.Humidity") + .getInt("ambientHumidityPercent"); + } + + if (jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.Connectivity").has("status")) { + deviceStatus = jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.Connectivity") + .getString("status"); + } + + if (jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.Fan").has("timerMode")) { + deviceFan = jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.Fan").getString("timerMode"); + } + + if (jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.ThermostatMode").has("mode")) { + deviceCurrentThermostatMode = jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.ThermostatMode") + .getString("mode"); + } + + if (jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.ThermostatMode").has("availableModes")) { + JSONArray jaAvailableModes = jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.ThermostatMode") + .getJSONArray("availableModes"); + + for (int nCount = 0; nCount < jaAvailableModes.length(); nCount++) { + // get Available Modes + deviceAvailableThermostatModes[nCount] = jaAvailableModes.getString(nCount); + } + } + + if (jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.ThermostatEco").has("mode")) { + deviceThermostatEcoMode = jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.ThermostatEco") + .getString("mode"); + } + + if (jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.ThermostatEco").has("heatCelsius")) { + deviceCurrentThermostatEcoHeatCelsius = jo.getJSONObject("traits") + .getJSONObject("sdm.devices.traits.ThermostatEco").getFloat("heatCelsius"); + } + + if (jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.ThermostatEco").has("coolCelsius")) { + deviceCurrentThermostatEcoCoolCelsius = jo.getJSONObject("traits") + .getJSONObject("sdm.devices.traits.ThermostatEco").getFloat("coolCelsius"); + } + + if (jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.ThermostatEco").has("availableModes")) { + JSONArray jaAvailableEcoModes = jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.ThermostatEco") + .getJSONArray("availableModes"); + + for (int nCount = 0; nCount < jaAvailableEcoModes.length(); nCount++) { + // get Available Modes + deviceAvailableThermostatEcoModes[nCount] = jaAvailableEcoModes.getString(nCount); + } + } + + if (jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.ThermostatHvac").has("status")) { + deviceThermostatHVACStatus = jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.ThermostatHvac") + .getString("status"); + } + + if (jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.Settings").has("temperatureScale")) { + + deviceTemperatureScaleSetting = jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.Settings") + .getString("temperatureScale"); + } + + if ((jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.ThermostatTemperatureSetpoint") + .has("heatCelsius")) + && (jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.ThermostatTemperatureSetpoint") + .has("coolCelsius"))) { + + deviceCurrentThermostatHeatCelsius = jo.getJSONObject("traits") + .getJSONObject("sdm.devices.traits.ThermostatTemperatureSetpoint").getFloat("heatCelsius"); + deviceCurrentThermostatCoolCelsius = jo.getJSONObject("traits") + .getJSONObject("sdm.devices.traits.ThermostatTemperatureSetpoint").getFloat("coolCelsius"); + // logger.debug("Before temp change min {} max {}", deviceCurrentThermostatHeatCelsius, + // deviceCurrentThermostatCoolCelsius); + } // ThermostatMode = HEATCOOL + else if (jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.ThermostatTemperatureSetpoint") + .has("coolCelsius")) { + deviceCurrentThermostatCoolCelsius = jo.getJSONObject("traits") + .getJSONObject("sdm.devices.traits.ThermostatTemperatureSetpoint").getFloat("coolCelsius"); + // logger.debug("Before temp change TargetTemp {}", deviceCurrentThermostatCoolCelsius); + } // ThermostatMode = Off or Heater + else if (jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.ThermostatTemperatureSetpoint") + .has("heatCelsius")) { + deviceCurrentThermostatHeatCelsius = jo.getJSONObject("traits") + .getJSONObject("sdm.devices.traits.ThermostatTemperatureSetpoint").getFloat("heatCelsius"); + // logger.debug("Before temp change TargetTemp {}", deviceCurrentThermostatHeatCelsius); + } // ThermostatMode = Off or AC + + if (jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.Temperature") + .has("ambientTemperatureCelsius")) { + deviceAmbientTemperatureSetting = jo.getJSONObject("traits").getJSONObject("sdm.devices.traits.Temperature") + .getFloat("ambientTemperatureCelsius"); + } + + JSONArray jaParentRelations = jo.getJSONArray("parentRelations"); + + for (int nCount = 0; nCount < jaParentRelations.length(); nCount++) { + // get Available Modes + deviceParentRelations[0] = jaParentRelations.getJSONObject(nCount).getString("parent"); + deviceParentRelations[1] = jaParentRelations.getJSONObject(nCount).getString("displayName"); + break; + } + deviceName = deviceParentRelations[1]; + + // last thing is to aggregate temperature settings for ease of use + if ((deviceCurrentThermostatMode.equalsIgnoreCase("HEAT")) + && (!deviceThermostatEcoMode.equalsIgnoreCase("MANUAL_ECO"))) { + deviceTargetTemperature = deviceCurrentThermostatHeatCelsius; + // logger.debug("updating heat.. {}", deviceTargetTemperature); + } else if ((deviceCurrentThermostatMode.equalsIgnoreCase("COOL")) + && (!deviceThermostatEcoMode.equalsIgnoreCase("MANUAL_ECO"))) { + deviceTargetTemperature = deviceCurrentThermostatCoolCelsius; + // logger.debug("updating cool..{}", deviceTargetTemperature); + } else if (deviceThermostatEcoMode.equalsIgnoreCase("MANUAL_ECO")) { + deviceMinTemperature = deviceCurrentThermostatEcoHeatCelsius; + deviceMaxTemperature = deviceCurrentThermostatEcoCoolCelsius; + // logger.debug("After temp change Eco TargetTemp {} min {} max {}", deviceTargetTemperature, + // deviceMinTemperature, deviceMaxTemperature); + } else if ((deviceCurrentThermostatMode.equalsIgnoreCase("HEATCOOL")) + && (!deviceThermostatEcoMode.equalsIgnoreCase("MANUAL_ECO"))) { + deviceMinTemperature = deviceCurrentThermostatHeatCelsius; + deviceMaxTemperature = deviceCurrentThermostatCoolCelsius; + // logger.debug("After temp change TargetTemp {} min {} max {}", deviceTargetTemperature, + // deviceMinTemperature, + // deviceMaxTemperature); + } + + return (true); + } + + public boolean setThermostatMode(String setting) throws IOException { + String jsonContent = "{\"command\" : \"sdm.devices.commands.ThermostatMode.SetMode\",\"params\" : {\"mode\" : \"" + + setting + "\"}}"; + + try { + String jsonResponse = nestUtility.deviceExecuteCommand(thing.getProperties().get("deviceId"), + thing.getProperties().get("projectId"), thing.getProperties().get("accessToken"), jsonContent); + return (true); + } catch (IOException e) { + throw new IOException(e.getMessage()); + } + } + + public boolean setThermostatEcoMode(String setting) throws IOException { + String jsonContent = "{\"command\" : \"sdm.devices.commands.ThermostatEco.SetMode\",\"params\" : {\"mode\" : \"" + + setting + "\"}}"; + try { + String jsonResponse = nestUtility.deviceExecuteCommand(thing.getProperties().get("deviceId"), + thing.getProperties().get("projectId"), thing.getProperties().get("accessToken"), jsonContent); + return (true); + } catch (IOException e) { + throw new IOException(e.getMessage()); + } + } + + public boolean setThermostatTargetTemperature(double value, double minValue, double maxValue, boolean typeRange) + throws IOException { + + String jsonContent = ""; + + try { + + if (!typeRange) { + if ((getTemperatureScaleSetting().equalsIgnoreCase("FAHRENHEIT"))) { + value = convertToCelsius(value); + } + if (getThermostatMode().equalsIgnoreCase("COOL")) { + jsonContent = "{\"command\" : \"sdm.devices.commands.ThermostatTemperatureSetpoint.SetCool\",\"params\" : {\"coolCelsius\" : " + + String.valueOf(value) + "}}"; + } else if (getThermostatMode().equalsIgnoreCase("HEAT")) { + jsonContent = "{\"command\" : \"sdm.devices.commands.ThermostatTemperatureSetpoint.SetHeat\",\"params\" : {\"heatCelsius\" : " + + String.valueOf(value) + "}}"; + } else { + // INVALID use case for setThermostatTargetTemperature.. + return (false); + } + } else { + if ((getTemperatureScaleSetting().equalsIgnoreCase("FAHRENHEIT"))) { + minValue = convertToCelsius(minValue); + maxValue = convertToCelsius(maxValue); + } + if (getThermostatMode().equalsIgnoreCase("HEATCOOL")) { + + jsonContent = "{\"command\" : \"sdm.devices.commands.ThermostatTemperatureSetpoint.SetRange\",\"params\" : {\"heatCelsius\" : " + + String.valueOf(minValue) + ",\"coolCelsius\" : " + String.valueOf(maxValue) + "}}"; + } else { + // INVALID use case for setThermostatTargetTemperature.. + return (false); + } + } + + nestUtility.deviceExecuteCommand(thing.getProperties().get("deviceId"), + thing.getProperties().get("projectId"), thing.getProperties().get("accessToken"), jsonContent); + return (true); + } catch (IOException e) { + throw new IOException(e.getMessage()); + } + } + + public boolean getDevices() throws IOException { + try { + String url = "https://smartdevicemanagement.googleapis.com/v1/enterprises/" + + thing.getProperties().get("projectId") + "/devices"; + nestUtility.getDeviceInfo(url); + return (true); + } catch (IOException e) { + throw new IOException(e.getMessage()); + } + } + + public boolean initializeThermostat() throws IOException { + + return (getDevices()); + } + + public int getCurrentHumidity() { + return (deviceHumidityPercent); + } + + public String getDeviceName() { + return (deviceName); + } + + public String getCustomName() { + return (deviceCustomName); + } + + public String getDeviceStatus() { + return (deviceStatus); + } + + public String getDeviceFan() { + return (deviceFan); + } + + public String getThermostatMode() { + return (deviceCurrentThermostatMode); + } + + public String[] getAvailableThermostatModes() { + return (deviceAvailableThermostatModes); + } + + public String getThermostatEcoMode() { + return (deviceThermostatEcoMode); + } + + public String[] getAvailableThermostatEcoModes() { + return (deviceAvailableThermostatEcoModes); + } + + public double getCurrentThermostatEcoHeatCelsius() { + if (getTemperatureScaleSetting().equalsIgnoreCase("Fahrenheit")) { + return (convertToFahrenheit(deviceCurrentThermostatEcoHeatCelsius)); + } else { + return (deviceCurrentThermostatEcoHeatCelsius); + } + } + + public double getCurrentThermostatEcoCoolCelsius() { + if (getTemperatureScaleSetting().equalsIgnoreCase("Fahrenheit")) { + return (convertToFahrenheit(deviceCurrentThermostatEcoCoolCelsius)); + } else { + return (deviceCurrentThermostatEcoCoolCelsius); + } + } + + public String getThermostatHVACStatus() { + return (deviceThermostatHVACStatus); + } + + public String getTemperatureScaleSetting() { + return (deviceTemperatureScaleSetting); + } + + public double getAmbientTemperatureSetting() { + if (getTemperatureScaleSetting().equalsIgnoreCase("Fahrenheit")) { + return (convertToFahrenheit(deviceAmbientTemperatureSetting)); + } else { + return (deviceAmbientTemperatureSetting); + } + } + + public double getCurrentTemperatureHeat() { + if (getTemperatureScaleSetting().equalsIgnoreCase("Fahrenheit")) { + return (convertToFahrenheit(deviceCurrentThermostatHeatCelsius)); + } else { + return (deviceCurrentThermostatHeatCelsius); + } + } + + public double getCurrentTemperatureCool() { + if (getTemperatureScaleSetting().equalsIgnoreCase("Fahrenheit")) { + return (convertToFahrenheit(deviceCurrentThermostatCoolCelsius)); + } else { + return (deviceCurrentThermostatCoolCelsius); + } + } + + public double getTargetTemperature() { + if (getTemperatureScaleSetting().equalsIgnoreCase("Fahrenheit")) { + return (convertToFahrenheit(deviceTargetTemperature)); + } else { + return (deviceTargetTemperature); + } + } + + public double[] getMinMaxTemperature() { + double[] minMaxValue = new double[2]; + if (getTemperatureScaleSetting().equalsIgnoreCase("Fahrenheit")) { + + minMaxValue[0] = convertToFahrenheit(deviceMinTemperature); + minMaxValue[1] = convertToFahrenheit(deviceMaxTemperature); + return (minMaxValue); + } else { + minMaxValue[0] = deviceMinTemperature; + minMaxValue[1] = deviceMaxTemperature; + return (minMaxValue); + } + } + + private double convertToFahrenheit(double temperature) { + return (((temperature / 5) * 9) + 32); + } + + private double convertToCelsius(double temperature) { + return ((temperature - 32) * 5 / 9); + } + + public boolean getThermostatInfo() throws IOException { + try { + String jsonContent; + jsonContent = nestUtility.getDeviceInfo("https://smartdevicemanagement.googleapis.com/v1/enterprises/" + + thing.getProperties().get("projectId") + "/devices/" + thing.getProperties().get("deviceId")); + + return (parseThermostatInfo(jsonContent)); + + } catch (IOException e) { + throw new IOException(e.getMessage()); + } + } +} diff --git a/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/thermostat/NestThermostatHandler.java b/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/thermostat/NestThermostatHandler.java new file mode 100644 index 0000000000000..80002a323b8d2 --- /dev/null +++ b/bundles/org.openhab.binding.nestdeviceaccess/src/main/java/org/openhab/binding/nestdeviceaccess/internal/thermostat/NestThermostatHandler.java @@ -0,0 +1,237 @@ +/** + * 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.nestdeviceaccess.internal.thermostat; + +import static org.openhab.binding.nestdeviceaccess.internal.nestdeviceaccessBindingConstants.*; + +import java.io.IOException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nestdeviceaccess.internal.nestdeviceaccessConfiguration; +import org.openhab.binding.nestdeviceaccess.internal.nesthelper.NestUtility; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link NestThermostatHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Brian Higginbotham - Initial contribution + */ +@NonNullByDefault +public class NestThermostatHandler extends BaseThingHandler { + + NestUtility nestUtility = new NestUtility(thing); + private final Logger logger = LoggerFactory.getLogger(NestThermostatHandler.class); + + private @NonNullByDefault({}) ScheduledFuture refreshJob; + private @Nullable nestdeviceaccessConfiguration config; + NestThermostat nestThermostat; + + public NestThermostatHandler(Thing thing) { + super(thing); + nestThermostat = new NestThermostat(thing); // initialize thermostat with base properties + } + + @Override + public void dispose() { + if (refreshJob != null) { + refreshJob.cancel(true); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.debug("handleCommand reporting {},{}", channelUID.getId(), command.toString()); + + try { + + if (thermostatName.equals(channelUID.getId())) { + + if (command instanceof RefreshType) { + // TODO: handle data refresh + + logger.debug("handleCommand reporting {}", command.toString()); + } + + // TODO: handle command + + // Note: if communication with thing fails for some reason, + // indicate that by setting the status with detail information: + // updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + // "Could not control device at IP address x.x.x.x"); + } else if (thermostatCurrentMode.equals(channelUID.getId())) { + if (command.toString() != "REFRESH") { + if (nestThermostat.setThermostatMode(command.toString())) { + logger.info("Thermostat: {} set command {} to {}", thing.getProperties().get("deviceName"), + channelUID.toString(), command.toString()); + Thread.sleep(3000); // set up to handle delay with changing mode and current settings being + // updated + refreshChannels(); + } + } + } else if (thermostatCurrentEcoMode.equals(channelUID.getId())) { + if (command.toString() != "REFRESH") { + if (nestThermostat.setThermostatEcoMode(command.toString())) { + logger.info("Thermostat: {} set command {} to {}", thing.getProperties().get("deviceName"), + channelUID.toString(), command.toString()); + Thread.sleep(3000); // set up to handle delay with changing mode and current settings being + // updated + refreshChannels(); + } + } + } else if (thermostatTargetTemperature.equals(channelUID.getId())) { + if (command.toString() != "REFRESH") { + if (nestThermostat.setThermostatTargetTemperature(Double.parseDouble(command.toString()), 0, 0, + false)) { + logger.info("Thermostat: {} set command {} to {}", thing.getProperties().get("deviceName"), + channelUID.toString(), command.toString()); + refreshChannels(); + } + } + } else if (thermostatMinimumTemperature.equals(channelUID.getId())) { + if (command.toString() != "REFRESH") { + if (nestThermostat.setThermostatTargetTemperature(0, Double.parseDouble(command.toString()), + nestThermostat.getMinMaxTemperature()[1], true)) { + logger.info("Thermostat: {} set command {} to {}", thing.getProperties().get("deviceName"), + channelUID.toString(), command.toString()); + refreshChannels(); + } + } + } else if (thermostatMaximumTemperature.equals(channelUID.getId())) { + if (command.toString() != "REFRESH") { + if (nestThermostat.setThermostatTargetTemperature(0, nestThermostat.getMinMaxTemperature()[0], + Double.parseDouble(command.toString()), true)) { + logger.info("Thermostat: {} set command {} to {}", thing.getProperties().get("deviceName"), + channelUID.toString(), command.toString()); + refreshChannels(); + } + } + } + } catch (IOException e) { + logger.debug("handleMessage reporting exception {}", e.getMessage()); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + logger.debug("handleMessage reporting exception {}", e.getMessage()); + } + } + + void refreshChannels() { + logger.info("refreshChannels process a timer for Updating thing Channels for:{}", thing.getUID()); + + try { + // refresh status of the device + + if (nestThermostat.getThermostatInfo()) { + State statusState = new StringType(nestThermostat.getDeviceStatus()); + updateState("thermostatStatus", statusState); + State nameState = new StringType(nestThermostat.getDeviceName()); + updateState("thermostatName", nameState); + State humidityState = new DecimalType(nestThermostat.getCurrentHumidity()); + updateState("thermostatHumidityPercent", humidityState); + State ambientState = new DecimalType(nestThermostat.getAmbientTemperatureSetting()); + updateState("thermostatAmbientTemperature", ambientState); + State setTempHeatState = new DecimalType(nestThermostat.getCurrentTemperatureHeat()); + updateState("thermostatTemperatureHeat", setTempHeatState); + State setTempCoolState = new DecimalType(nestThermostat.getCurrentTemperatureCool()); + updateState("thermostatTemperatureCool", setTempCoolState); + State setTargetTempState = new DecimalType(nestThermostat.getTargetTemperature()); + updateState("thermostatTargetTemperature", setTargetTempState); + State setMinTempState = new DecimalType(nestThermostat.getMinMaxTemperature()[0]); + updateState("thermostatMinimumTemperature", setMinTempState); + State setMaxTempState = new DecimalType(nestThermostat.getMinMaxTemperature()[1]); + updateState("thermostatMaximumTemperature", setMaxTempState); + State modeState = new StringType(nestThermostat.getThermostatMode()); + updateState("thermostatCurrentMode", modeState); + State ecoModeState = new StringType(nestThermostat.getThermostatEcoMode()); + updateState("thermostatCurrentEcoMode", ecoModeState); + State scaleSettingState = new StringType(nestThermostat.getTemperatureScaleSetting()); + updateState("thermostatScaleSetting", scaleSettingState); + + } + } catch (IOException e) { + logger.debug("refreshChannels() reporting exception {}", e.getMessage()); + } + } + + @Override + public void initialize() { + logger.debug("Start initializing!"); + config = getConfigAs(nestdeviceaccessConfiguration.class); + config.projectId = thing.getProperties().get("projectId"); + config.clientId = thing.getProperties().get("clientId"); + config.clientSecret = thing.getProperties().get("clientSecret"); + config.accessToken = thing.getProperties().get("accessToken"); + config.refreshToken = thing.getProperties().get("refreshToken"); + config.deviceId = thing.getProperties().get("deviceId"); + config.deviceName = thing.getProperties().get("deviceName"); + config.refreshInterval = Integer.parseInt(thing.getConfiguration().get("refreshInterval").toString()); + + // TODO: Initialize the handler. + // The framework requires you to return from this method quickly. Also, before leaving this method a thing + // status from one of ONLINE, OFFLINE or UNKNOWN must be set. This might already be the real thing status in + // case you can decide it directly. + // In case you can not decide the thing status directly (e.g. for long running connection handshake using WAN + // access or similar) you should set status UNKNOWN here and then decide the real status asynchronously in the + // background. + + // set the thing status to UNKNOWN temporarily and let the background task decide for the real status. + // the framework is then able to reuse the resources from the thing handler initialization. + // we set this upfront to reliably check status updates in unit tests. + + updateStatus(ThingStatus.UNKNOWN); + + // Example for background initialization: + scheduler.execute(() -> { + boolean thingReachable = true; // + try { + + nestThermostat.initializeThermostat(); + // NestUtility.pubSubEventHandler("openhab-nest-int-1601138253554", "sdm_pull_events"); + // when done do: + thingReachable = nestThermostat.getDeviceStatus().equalsIgnoreCase("ONLINE"); + if (thingReachable) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE); + } + + } catch (Exception e) { + logger.debug("Initialize() caught an exception {}", e.getMessage()); + } + }); + + if (refreshJob == null || refreshJob.isCancelled()) { + refreshJob = scheduler.scheduleWithFixedDelay(this::refreshChannels, 0, config.refreshInterval, + TimeUnit.SECONDS); + } + // Note: When initialization can NOT be done set the status with more details for further + // analysis. See also class ThingStatusDetail for all available status details. + // Add a description to give user information to understand why thing does not work as expected. E.g. + // updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + // "Can not access device as username and/or password are invalid"); + } +} diff --git a/bundles/org.openhab.binding.nestdeviceaccess/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.nestdeviceaccess/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 0000000000000..8512317ff5ecf --- /dev/null +++ b/bundles/org.openhab.binding.nestdeviceaccess/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + Nest Device Access Binding + This is the binding for the SDM Google API to integrate Nest products. + Brian Higginbotham + + diff --git a/bundles/org.openhab.binding.nestdeviceaccess/src/main/resources/OH-INF/i18n/nestdeviceaccess_xx_XX.properties b/bundles/org.openhab.binding.nestdeviceaccess/src/main/resources/OH-INF/i18n/nestdeviceaccess_xx_XX.properties new file mode 100644 index 0000000000000..8b07da3a7620e --- /dev/null +++ b/bundles/org.openhab.binding.nestdeviceaccess/src/main/resources/OH-INF/i18n/nestdeviceaccess_xx_XX.properties @@ -0,0 +1,17 @@ +# FIXME: please substitute the xx_XX with a proper locale, ie. de_DE +# FIXME: please do not add the file to the repo if you add or change no content +# binding +binding.nestdeviceaccess.name = +binding.nestdeviceaccess.description = + +# thing types +thing-type.nestdeviceaccess.sample.label = +thing-type.nestdeviceaccess.sample.description = + +# thing type config description +thing-type.config.nestdeviceaccess.sample.config1.label = +thing-type.config.nestdeviceaccess.sample.config1.description = + +# channel types +channel-type.nestdeviceaccess.sample-channel.label = +channel-type.nestdeviceaccess.sample-channel.description = diff --git a/bundles/org.openhab.binding.nestdeviceaccess/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.nestdeviceaccess/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..df925f89b1772 --- /dev/null +++ b/bundles/org.openhab.binding.nestdeviceaccess/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,127 @@ + + + + + + A binding to interact with Nest Device Access + + + + This is the GCP Project that you created and integrated with the SDM API + + + + This is the oAuth 2.0 Client ID associated with the provided Project ID + + + + This is the Client Secret returned when setting up your oAuth 2.0 Client ID + + + + This is the one time authorization token used to retrieve the Refresh and Access token to the SDM API + + + + + + A binding to interact with Nest Device DoorBells + + + + + + + This is refresh interval in seconds to update the nest device information + + + + + String + + DoorBell Name + + + + + A binding to interact with Nest Device Thermostats + + + + + + + + + + + + + + + + + This is refresh interval in seconds to update the nest device information + + + + + String + + Thermostat Name + + + Number:Length + + Lists the current humidity percentage from the thermostat + + + Number:Dimensionless + + Lists the current ambient temperature from the thermostat + + + Number:Dimensionless + + Lists the Cool Temperature Setting from the thermostat + + + Number:Dimensionless + + Lists the Heat Temperature Setting from the thermostat + + + Number:Dimensionless + + Lists the Target Temperature Setting from the thermostat + + + Number:Dimensionless + + Lists the Minimum Temperature Setting from the thermostat + + + Number:Dimensionless + + Lists the Maximum Temperature Setting from the thermostat + + + String + + Lists the current mode from the thermostat + + + String + + Lists the scale setting from the thermostat + + + String + + Lists the current Eco mode from the thermostat + + +