Trying Google Nest API with Postman and Python
Tags: google, nest, python, iot
Published: 2023-03-06 04:56:05

---
title: Trying Google Nest API with Postman and Python
published: true
description: Getting started with the Google Nest API with Postman and Python.
tags: google,nest,python,IOT
cover_image: https://geoffhudik.com/wp-content/uploads/2023/03/cover.png
canonical_url: https://geoffhudik.com/tech/2023/03/04/trying-google-nest-api-with-postman-and-python/
---

A side project of mine requires automating my Google Nest thermostat so I decided to try out the [Smart Device Management (SDM) API](https://developers.google.com/nest/device-access/api/thermostat).

### Setup

I began with [Nest's Get Started Guide](https://developers.google.com/nest/device-access/get-started). That walks through the process in detail so I'll just recap it here and provide more context in places.

Registration created a project in the [Nest Device Access Console](https://console.nest.google.com/device-access) where I initially skipped the OAuth Client Id and Pub/Sub events setup.

Next I [created a GCP project](https://cloud.google.com/resource-manager/docs/creating-managing-projects) to access the SDM API through. Afterwards I went to _APIs & Services > Credentials_ and clicked _Create Credentials > OAuth client ID_ and set it up as a _Web application_ with URIs including `https://www.google.com`, `https://www.getpostman.com/oauth2/callback`, and `http://localhost:8080/`.

[![GCP OAuth Credentials](https://geoffhudik.com/wp-content/uploads/2023/02/gcp-creds-2.png)](https://geoffhudik.com/wp-content/uploads/2023/02/gcp-creds-2.png)

With the OAuth credentials created, I copied the _Client ID_ into the [Device Access Console](https://console.nest.google.com/device-access) project.

[![Device Access Console](https://geoffhudik.com/wp-content/uploads/2023/02/device-access-console-2.png)](https://geoffhudik.com/wp-content/uploads/2023/02/device-access-console-2.png)

The last step was [linking my Nest Google account to the Device Access project](https://developers.google.com/nest/device-access/authorize), opening this URL:

<pre>https://nestservices.google.com/partnerconnections/{project-id}/auth
    ?redirect_uri=https://www.google.com
    &access_type=offline
    &prompt=consent
    &client_id={oauth2-client-id}
    &response_type=code
    &scope=https://www.googleapis.com/auth/sdm.service
</pre>

### Postman Configuration

For simplicity I started off with [Postman](https://www.postman.com/) to test API calls. First I setup some variables at the collection level to reduce later repetition and hardcoding.

*   `{{base-url}}`: https://smartdevicemanagement.googleapis.com/v1
*   `{{client-id}}`: from [GCP OAuth credentials](https://console.cloud.google.com/apis/credentials)
*   `{{client-secret}}`: from [GCP OAuth credentials](https://console.cloud.google.com/apis/credentials)
*   `{{project-id}}`: from [Device Access Console](https://console.nest.google.com/device-access)

[![Postman Variables](https://geoffhudik.com/wp-content/uploads/2023/02/postman-vars.png)](https://geoffhudik.com/wp-content/uploads/2023/02/postman-vars.png)

Likewise, authorization was setup at the collection level:

*   Grant Type: Authorization Code
*   Callback URL: https://www.getpostman.com/oauth2/callback
*   Auth URL: https://accounts.google.com/o/oauth2/auth
*   Access Token URL: https://accounts.google.com/o/oauth2/token
*   Client ID: `{{client-id}}` variable from [GCP OAuth credentials](https://console.cloud.google.com/apis/credentials)
*   Client Secret: `{{client-secret}}` variable from [GCP OAuth credentials](https://console.cloud.google.com/apis/credentials)
*   Scope: https://www.googleapis.com/auth/sdm.service

[![Postman Authorization](https://geoffhudik.com/wp-content/uploads/2023/02/postman-auth.png)](https://geoffhudik.com/wp-content/uploads/2023/02/postman-auth.png)

[![Postman Use Token](https://geoffhudik.com/wp-content/uploads/2023/02/postman-use-token.png)](https://geoffhudik.com/wp-content/uploads/2023/02/postman-use-token.png)

### Postman Tests

With the auth token in use, the first logical SDM endpoint to call was [Device list](https://developers.google.com/nest/device-access/api#list), `{{base-url}}/enterprises/{{project-id}}/devices/` to retrieve authorized device(s).

[![Postman Device List](https://geoffhudik.com/wp-content/uploads/2023/02/postman-device-list-2.png)](https://geoffhudik.com/wp-content/uploads/2023/02/postman-device-list-2.png)

Device list produced output similar to the following.

```javascript
{
    "devices": [
        {
            "name": "enterprises/{{project-id}}/devices/{{device-id}}",
            "type": "sdm.devices.types.THERMOSTAT",
            "assignee": "enterprises/{{project-id}}/structures/{{structure-id}}/rooms/{{room-id}}",
            "traits": {
                "sdm.devices.traits.Info": {
                    "customName": ""
                },
                "sdm.devices.traits.Humidity": {
                    "ambientHumidityPercent": 45
                },
                "sdm.devices.traits.Connectivity": {
                    "status": "ONLINE"
                },
                "sdm.devices.traits.Fan": {
                    "timerMode": "OFF"
                },
                "sdm.devices.traits.ThermostatMode": {
                    "mode": "HEAT",
                    "availableModes": [
                        "HEAT",
                        "COOL",
                        "HEATCOOL",
                        "OFF"
                    ]
                },
                "sdm.devices.traits.ThermostatEco": {
                    "availableModes": [
                        "OFF",
                        "MANUAL_ECO"
                    ],
                    "mode": "OFF",
                    "heatCelsius": 15.458176,
                    "coolCelsius": 26.784546
                },
                "sdm.devices.traits.ThermostatHvac": {
                    "status": "OFF"
                },
                "sdm.devices.traits.Settings": {
                    "temperatureScale": "FAHRENHEIT"
                },
                "sdm.devices.traits.ThermostatTemperatureSetpoint": {
                    "heatCelsius": 24.473785
                },
                "sdm.devices.traits.Temperature": {
                    "ambientTemperatureCelsius": 24.709991
                }
            },
            "parentRelations": [
                {
                    "parent": "enterprises/{{project-id}}/structures/{{structure-id}}/rooms/{{room-id}}",
                    "displayName": "Thermostat"
                }
            ]
        }
    ]
}
```

The `{{device-id}}` in the `{{name}}` field of the list response I then used as the key to make API calls against the thermostat, plugging the value into a new `{{device-id}}` Postman collection variable.

[Changing the temperature](https://developers.google.com/nest/device-access/api/thermostat#change_the_temperature_setpoints) was the next logical test for me: _POST `{{base-url}}`/enterprises/`{{project-id}}`/devices/`{{device-id}}`:executeCommand_

[![Postman Set Temp](https://geoffhudik.com/wp-content/uploads/2023/02/postman-set-temp-2.png)](https://geoffhudik.com/wp-content/uploads/2023/02/postman-set-temp-2.png)

### Coding it up

With requests working in Postman, it was time to write some code. My project idea involves the use of a [Raspberry Pi](https://www.raspberrypi.org/) and a [Python](https://www.python.org/) library so I decided to start with Python. I'll preface this by saying I've done very little Python and it probably shows, plus this is only discovery work.

I started off with `pip3 install` for these key packages:

*   [click](https://palletsprojects.com/p/click/)
*   [google-api-core](https://github.com/googleapis/python-api-core)
*   [google-api-python-client](https://github.com/googleapis/google-api-python-client/)
*   [google-auth](https://github.com/googleapis/google-auth-library-python)
*   [google-auth-oauthlib](https://github.com/googleapis/google-auth-library-python-oauthlib)

The main script makes use of [Click](https://click.palletsprojects.com/en/8.1.x/) for a quick command line interface. This allowed me to quickly run commands like: `python3 main.py temp 75 --mode Cool`

```python
import click

from env import get_project_id
from thermostat import Thermostat, ThermostatMode

@click.group()
@click.option('--debug/--no-debug', default=False)
@click.pass_context
def cli(ctx, debug):
    ctx.ensure_object(dict)
    ctx.obj['DEBUG'] = debug

    thermostat = Thermostat(projectId=get_project_id(), deviceName=None, debug=debug)
    thermostat.initialize()
    ctx.obj['thermostat'] = thermostat

    pass

@cli.command()
@click.pass_context
@click.option('--mode', required=True,
              type=click.Choice(['Cool', 'Heat'], case_sensitive=True))
@click.argument('temp', nargs=1, type=click.FLOAT)
def temp(ctx, temp: float, mode):
    modeType = ThermostatMode[mode]
    ctx.obj['thermostat'].set_temp(modeType, temp)

cli.add_command(temp)

if __name__ == '__main__':
    cli()
```

The main functionality is handled by the `Thermostat` class. On creation it creates a service object using the [Google Python API Client](https://github.com/googleapis/google-api-python-client/). That's used later to help build and execute API requests. At app exit the service object is closed.

```python
import atexit
import json

from credentials import get_credentials_installed
from enum import Enum
from googleapiclient.discovery import build
from temperature import celsius_to_fahrenheit, fahrenheit_to_celsius
from urllib.error import HTTPError

ThermostatMode = Enum('ThermostatMode', ['Cool', 'Heat'])

class Thermostat:
  def __init__(self, projectId, deviceName, debug):
    self.projectId = projectId
    self.projectParent = f"enterprises/{projectId}"
    self.deviceName = deviceName
    self.debug = debug
    credentials = get_credentials_installed()
    self.service = build(serviceName='smartdevicemanagement', version='v1', credentials=credentials)
    atexit.register(self.cleanup)

  def cleanup(self):
    self.service.close()
    print('Service closed')

# ...
```

`Thermostat` creation also kicks off credentials setup. There are [different OAuth flows](https://github.com/googleapis/google-api-python-client/blob/main/docs/oauth.md#flows) available. To start with I chose Installed App Flow, downloading OAuth Credentials from [GCP OAuth credentials](https://console.cloud.google.com/apis/credentials) to a Git ignored `./secrets/client_secret.json` file. The first time it runs there's an interactive login step. After that the credentials are [pickled](https://docs.python.org/3/library/pickle.html) into a `secrets` directory file.

[![Initial auth with Google](https://geoffhudik.com/wp-content/uploads/2023/03/auth-initial.png)](https://geoffhudik.com/wp-content/uploads/2023/03/auth-initial.png)

```python
import pickle
import os
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request

def get_credentials_installed():
  SCOPES = ['https://www.googleapis.com/auth/sdm.service']

  credentials = None

  pickle_file = './secrets/token.pickle'

  if os.path.exists(pickle_file):
        with open(pickle_file, 'rb') as token:
            credentials = pickle.load(token)

  if not credentials or not credentials.valid:
        if credentials and credentials.expired and credentials.refresh_token:
            credentials.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file('./secrets/client_secret.json', SCOPES)
            credentials = flow.run_local_server()

        with open(pickle_file, 'wb') as token:
            pickle.dump(credentials, token)

  return credentials
```

Later I plan to look into a service account more but for now Installed App Flow fits my needs.

After `Thermostat` creation, initialization gets the list of devices, resolves the target device, and reads and prints current thermostat settings.

```python
def initialize(self):
    request = self.service.enterprises().devices().list(parent=self.projectParent)
    response = self.__execute(request)
    device = self.__get_device(response)

    traits = device['traits']
    self.deviceName = device['name']
    self.mode = traits['sdm.devices.traits.ThermostatMode']['mode']
    self.tempC = traits['sdm.devices.traits.Temperature']['ambientTemperatureCelsius']
    self.tempF = celsius_to_fahrenheit(self.tempC)

    setpointTrait = traits['sdm.devices.traits.ThermostatTemperatureSetpoint']
    key = f'{self.mode.lower()}Celsius'
    self.setpointC = setpointTrait[key]
    self.setpointF = celsius_to_fahrenheit(self.setpointC)

    print(f'Nest mode is {self.mode}, ' +
          f'temp is {round(self.tempF, 0)} °F, ' +
          f'setpoint is {round(self.setpointF, 0)} °F')
```

Initialization calls a couple helper functions. First is a wrapper around executing service requests.

```python
def __execute(self, request):
try:
  response = request.execute()

  if self.debug:
    print(json.dumps(response, sort_keys=True, indent=4))

  return response

except HTTPError as e:
  if self.debug:
    print('Error response status code : {0}, reason : {1}'.format(e.status_code, e.error_details))
  raise e
```

Second is one to resolve the thermostat device among the authorized device(s). If a device name is specified during creation, it looks for that name. If no device was specified it'll default the first device if there's only one.

```python
def __get_device(self, response):
    device = None
    device_count = len(response['devices'])

    if (self.deviceName) is not None:
      full_device_name = f"{self.projectParent}/devices/{self.deviceName}"

      for d in response['devices']:
        if d['name'] == full_device_name:
          device = d
          break

      if device is None:
        raise Exception("Failed find device by name")

    else:
      if device_count == 1:
        device = response['devices'][0]
      else:
        raise Exception(f'Found ${device_count} devices, expected 1')

    return device
```

Finally the more interesting bits include changing the [thermostat mode](https://developers.google.com/nest/device-access/traits/device/thermostat-mode)...

```python
def set_mode(self, mode: ThermostatMode):
    data = {
      "command": "sdm.devices.commands.ThermostatMode.SetMode",
      "params": {
        "mode": mode.name.upper()
      }
    }

    request = self.service.enterprises().devices().executeCommand(name=self.deviceName, body=data)
    response = self.__execute(request)
    print(f'Nest set to mode {mode.name}')
```

... and of most interest, [commands to set the temperature](https://developers.google.com/nest/device-access/traits/device/thermostat-temperature-setpoint#commands). It's worth noting that changing the temperature requires the thermostat to be in the correct mode so that needs to be changed or checked first.

```python
def set_temp(self, mode: ThermostatMode, tempF: float):
    self.set_mode(mode)
    tempC = fahrenheit_to_celsius(tempF)

    data = {
      "command": f"sdm.devices.commands.ThermostatTemperatureSetpoint.Set{mode.name}",
      "params": {
        f"{mode.name}Celsius": tempC
      }
    }

    request = self.service.enterprises().devices().executeCommand(name=self.deviceName, body=data)
    response = self.__execute(request)

    print(f'Nest set to temp {round(tempF, 0)} °F ({round(tempC, 0)} °C) for mode {mode.name}')
```

The full code can be found at [github.com/thnk2wn/google-nest-sandbox](https://github.com/thnk2wn/google-nest-sandbox).

A sample test looked like this. There are also some launch configurations in the `.vscode` folder of the repo. That's it for now, more interesting Nest integrations are in progress.

{% youtube eDPHoZz_6xw %}