diff --git a/.gitignore b/.gitignore index 9a292f3..9ddd6b0 100644 --- a/.gitignore +++ b/.gitignore @@ -89,4 +89,5 @@ ENV/ # Rope project settings .ropeproject -.vscode/ \ No newline at end of file +.vscode/ +.DS_Store diff --git a/custom_components/badnest/__init__.py b/custom_components/badnest/__init__.py index 8de517e..5bfedfe 100644 --- a/custom_components/badnest/__init__.py +++ b/custom_components/badnest/__init__.py @@ -3,14 +3,19 @@ import voluptuous as vol from homeassistant.helpers import config_validation as cv from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from .const import DOMAIN +from .const import DOMAIN, CONF_ISSUE_TOKEN, CONF_COOKIE, CONF_APIKEY CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( + DOMAIN: vol.All( { - vol.Required(CONF_EMAIL): cv.string, - vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_EMAIL, default=""): cv.string, + vol.Required(CONF_PASSWORD, default=""): cv.string, + }, + { + vol.Required(CONF_ISSUE_TOKEN, default=""): cv.string, + vol.Required(CONF_COOKIE, default=""): cv.string, + vol.Required(CONF_APIKEY, default=""): cv.string } ) }, @@ -23,10 +28,22 @@ def setup(hass, config): if config.get(DOMAIN) is not None: email = config[DOMAIN].get(CONF_EMAIL) password = config[DOMAIN].get(CONF_PASSWORD) + issue_token = config[DOMAIN].get(CONF_ISSUE_TOKEN) + cookie = config[DOMAIN].get(CONF_COOKIE) + api_key = config[DOMAIN].get(CONF_APIKEY) else: email = None password = None + issue_token = None + cookie = None + api_key = None - hass.data[DOMAIN] = {CONF_EMAIL: email, CONF_PASSWORD: password} + hass.data[DOMAIN] = { + CONF_EMAIL: email, + CONF_PASSWORD: password, + CONF_ISSUE_TOKEN: issue_token, + CONF_COOKIE: cookie, + CONF_APIKEY: api_key + } return True diff --git a/custom_components/badnest/api.py b/custom_components/badnest/api.py index 39c336b..2d1b19d 100644 --- a/custom_components/badnest/api.py +++ b/custom_components/badnest/api.py @@ -3,32 +3,71 @@ import requests API_URL = "https://home.nest.com" CAMERA_WEBAPI_BASE = "https://webapi.camera.home.nest.com" CAMERA_URL = "https://nexusapi-us1.camera.home.nest.com" +USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) " \ + "AppleWebKit/537.36 (KHTML, like Gecko) " \ + "Chrome/75.0.3770.100 Safari/537.36" +URL_JWT = "https://nestauthproxyservice-pa.googleapis.com/v1/issue_jwt" class NestAPI: - def __init__(self, email, password): + def __init__(self, email, password, issue_token, cookie, api_key): self._user_id = None self._access_token = None self._session = requests.Session() self._session.headers.update({"Referer": "https://home.nest.com/"}) self._device_id = None - self._login(email, password) + if email is not None and password is not None: + self._login_nest(email, password) + else: + self._login_google(issue_token, cookie, api_key) self.update() - def _login(self, email, password): + def _login_nest(self, email, password): r = self._session.post( f"{API_URL}/session", json={"email": email, "password": password} ) self._user_id = r.json()["userid"] self._access_token = r.json()["access_token"] + def _login_google(self, issue_token, cookie, api_key): + headers = { + 'Sec-Fetch-Mode': 'cors', + 'User-Agent': USER_AGENT, + 'X-Requested-With': 'XmlHttpRequest', + 'Referer': 'https://accounts.google.com/o/oauth2/iframe', + 'cookie': cookie + } + r = requests.get(url=issue_token, headers=headers) + access_token = r.json()['access_token'] + + headers = { + 'Authorization': 'Bearer ' + access_token, + 'User-Agent': USER_AGENT, + 'x-goog-api-key': api_key, + 'Referer': 'https://home.nest.com' + } + params = { + "embed_google_oauth_access_token": True, + "expire_after": '3600s', + "google_oauth_access_token": access_token, + "policy_id": 'authproxy-oauth-policy' + } + r = requests.post(url=URL_JWT, headers=headers, params=params) + self._user_id = r.json()['claims']['subject']['nestId']['id'] + self._access_token = r.json()['jwt'] + def update(self): raise NotImplementedError() class NestThermostatAPI(NestAPI): - def __init__(self, email, password): - super(NestThermostatAPI, self).__init__(email, password) + def __init__(self, email, password, issue_token, cookie, api_key): + super(NestThermostatAPI, self).__init__( + email, + password, + issue_token, + cookie, + api_key) self._shared_id = None self._czfe_url = None self._compressor_lockout_enabled = None @@ -182,8 +221,13 @@ class NestThermostatAPI(NestAPI): class NestCameraAPI(NestAPI): - def __init__(self, email, password): - super(NestCameraAPI, self).__init__(email, password) + def __init__(self, email, password, issue_token, cookie, api_key): + super(NestCameraAPI, self).__init__( + email, + password, + issue_token, + cookie, + api_key) # log into dropcam self._session.post( f"{API_URL}/dropcam/api/login", diff --git a/custom_components/badnest/camera.py b/custom_components/badnest/camera.py index 2cca102..674cdcf 100644 --- a/custom_components/badnest/camera.py +++ b/custom_components/badnest/camera.py @@ -7,7 +7,7 @@ from homeassistant.components.camera import Camera, SUPPORT_ON_OFF from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from .api import NestCameraAPI -from .const import DOMAIN +from .const import DOMAIN, CONF_ISSUE_TOKEN, CONF_COOKIE, CONF_APIKEY _LOGGER = logging.getLogger(__name__) @@ -25,7 +25,10 @@ async def async_setup_platform(hass, api = NestCameraAPI( hass.data[DOMAIN][CONF_EMAIL], - hass.data[DOMAIN][CONF_PASSWORD] + hass.data[DOMAIN][CONF_PASSWORD], + hass.data[DOMAIN][CONF_ISSUE_TOKEN], + hass.data[DOMAIN][CONF_COOKIE], + hass.data[DOMAIN][CONF_APIKEY] ) # cameras = await hass.async_add_executor_job(nest.get_cameras()) diff --git a/custom_components/badnest/climate.py b/custom_components/badnest/climate.py index 16852bc..7d5e56b 100644 --- a/custom_components/badnest/climate.py +++ b/custom_components/badnest/climate.py @@ -29,7 +29,7 @@ from homeassistant.const import ( ) from .api import NestThermostatAPI -from .const import DOMAIN +from .const import DOMAIN, CONF_ISSUE_TOKEN, CONF_COOKIE, CONF_APIKEY NEST_MODE_HEAT_COOL = "range" NEST_MODE_ECO = "eco" @@ -64,7 +64,10 @@ async def async_setup_platform(hass, """Set up the Nest climate device.""" nest = NestThermostatAPI( hass.data[DOMAIN][CONF_EMAIL], - hass.data[DOMAIN][CONF_PASSWORD] + hass.data[DOMAIN][CONF_PASSWORD], + hass.data[DOMAIN][CONF_ISSUE_TOKEN], + hass.data[DOMAIN][CONF_COOKIE], + hass.data[DOMAIN][CONF_APIKEY] ) async_add_entities([NestClimate(nest)]) diff --git a/custom_components/badnest/const.py b/custom_components/badnest/const.py index 2e5432f..ee62a48 100644 --- a/custom_components/badnest/const.py +++ b/custom_components/badnest/const.py @@ -1 +1,4 @@ DOMAIN = 'badnest' +CONF_ISSUE_TOKEN = 'issue_token' +CONF_COOKIE = 'cookie' +CONF_APIKEY = 'api_key' diff --git a/info.md b/info.md index 58c99a0..9e41424 100644 --- a/info.md +++ b/info.md @@ -13,20 +13,53 @@ This isn't an advertised or public API, it's still better than web scraping, but - Tested with a single thermostat, I have no other devices to test with - Camera integration is untested by me - Nest could change their webapp api at any time, making this defunct -- Won't work with Google-linked accounts - Presets don't work (Eco, Away) -## Example configuration.yaml +## Example configuration.yaml - When you're not using the Google Auth Login ```yaml badnest: email: email@domain.com password: !secret nest_password +climate: + - platform: badnest + scan_interval: 10 + camera: - platform: badnest +``` + +## Example configuration.yaml - When you are using the Google Auth Login + +```yaml +badnest: + issue_token: "https://accounts.google.com/o/oauth2/iframerpc....." + cookie: "OCAK=......" + api_key : "#YOURAPIKEYHERE#" climate: - platform: badnest scan_interval: 10 + +camera: + - platform: badnest ``` + +Google Login support added with many thanks to: chrisjshull from + +The values of `"issue_token"`, `"cookie"` and `"api_key"` are specific to your Google Account. To get them, follow these steps (only needs to be done once, as long as you stay logged into your Google Account). + +1. Open a Chrome browser tab in Incognito Mode (or clear your cache). +2. Open Developer Tools (View/Developer/Developer Tools). +3. Click on 'Network' tab. Make sure 'Preserve Log' is checked. +4. In the 'Filter' box, enter `issueToken` +5. Go to `home.nest.com`, and click 'Sign in with Google'. Log into your account. +6. One network call (beginning with `iframerpc`) will appear in the Dev Tools window. Click on it. +7. In the Headers tab, under General, copy the entire `Request URL` (beginning with `https://accounts.google.com`, ending with `nest.com`). This is your `"issue_token"` in `configuration.yaml`. +8. In the 'Filter' box, enter `oauth2/iframe` +9. Several network calls will appear in the Dev Tools window. Click on the last `iframe` call. +10. In the Headers tab, under Request Headers, copy the entire `cookie` (beginning `OCAK=...` - **include the whole string which is several lines long and has many field/value pairs** - do not include the `cookie:` name). This is your `"cookie"` in `configuration.yaml`. +11. In the 'Filter' box, enter `issue_jwt` +12. Click on the last `issue_jwt` call. +13. In the Headers tab, under Request Headers, copy the entire `x-goog-api-key` (do not include the `x-goog-api-key:` name). This is your `"api_key"` in `configuration.yaml`.