Skip to content

Commit

Permalink
Use cookie for authentication (#167)
Browse files Browse the repository at this point in the history
  • Loading branch information
Cruguah authored Apr 26, 2023
1 parent e3893e2 commit 4b3f2cc
Show file tree
Hide file tree
Showing 21 changed files with 744 additions and 300 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
"*.yaml": "home-assistant"
},
"python.formatting.provider": "black"
}
}
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ This integration will add the most important sensors of your Nest Protect device

- Only Google Accounts are supported, there is no plan to support legacy Nest accounts
- When Nest Protect (wired) occupancy is triggered, it will stay 'on' for 10 minutes. (API limitation)
- Only *cookie authentication* is supported as Google removed the API key authentication method. This means that you need to login to the Nest website at least once to generate a cookie. This cookie will be used to authenticate with the Nest API. The cookie will be stored in the Home Assistant configuration folder and will be used for future requests. If you logout from your browser or change your password, you need to reautenticate and and replace the current issue_token and cookies.

## Installation

Expand All @@ -33,6 +34,23 @@ Copy the `custom_components/nest_protect` to your custom_components folder. Rebo
[![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=nest_protect)


## Retrieving `issue_token` and `cookies`

(adapted from [homebridge-nest documentation](https://github.com/chrisjshull/homebridge-nest))

The values of "issue_token" and "cookies" 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). This is your `issue_token` in the configuration form.
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 (include the whole string which is several lines long and has many field/value pairs - do not include the cookie: name). This is your `cookies` in the configuration form.
11. Do not log out of home.nest.com, as this will invalidate your credentials. Just close the browser tab.

## Advanced

Expand Down
35 changes: 28 additions & 7 deletions custom_components/nest_protect/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,15 @@
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.dispatcher import async_dispatcher_send

from .const import CONF_ACCOUNT_TYPE, CONF_REFRESH_TOKEN, DOMAIN, LOGGER, PLATFORMS
from .const import (
CONF_ACCOUNT_TYPE,
CONF_COOKIES,
CONF_ISSUE_TOKEN,
CONF_REFRESH_TOKEN,
DOMAIN,
LOGGER,
PLATFORMS,
)
from .pynest.client import NestClient
from .pynest.const import NEST_ENVIRONMENTS
from .pynest.exceptions import (
Expand Down Expand Up @@ -52,13 +60,28 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Nest Protect from a config entry."""
refresh_token = entry.data[CONF_REFRESH_TOKEN]
issue_token = None
cookies = None
refresh_token = None

if CONF_ISSUE_TOKEN in entry.data and CONF_COOKIES in entry.data:
issue_token = entry.data[CONF_ISSUE_TOKEN]
cookies = entry.data[CONF_COOKIES]
if CONF_REFRESH_TOKEN in entry.data:
refresh_token = entry.data[CONF_REFRESH_TOKEN]
account_type = entry.data[CONF_ACCOUNT_TYPE]
session = async_get_clientsession(hass)
client = NestClient(session=session, environment=NEST_ENVIRONMENTS[account_type])

try:
auth = await client.get_access_token(refresh_token)
if issue_token and cookies:
auth = await client.get_access_token_from_cookies(issue_token, cookies)
elif refresh_token:
auth = await client.get_access_token_from_refresh_token(refresh_token)
else:
raise Exception(
"No cookies, issue token and refresh token, please provide issue_token and cookies or refresh_token"
)
nest = await client.authenticate(auth.access_token)
except (TimeoutError, ClientError) as exception:
raise ConfigEntryNotReady from exception
Expand Down Expand Up @@ -126,8 +149,6 @@ async def _async_subscribe_for_data(hass: HomeAssistant, entry: ConfigEntry, dat
"""Subscribe for new data."""
entry_data: HomeAssistantNestProtectData = hass.data[DOMAIN][entry.entry_id]

LOGGER.debug("Subscriber: listening for new data")

try:
# TODO move refresh token logic to client
if (
Expand All @@ -138,8 +159,8 @@ async def _async_subscribe_for_data(hass: HomeAssistant, entry: ConfigEntry, dat

if not entry_data.client.auth or entry_data.client.auth.is_expired():
LOGGER.debug("Subscriber: retrieving new Google access token")
await entry_data.client.get_access_token()
await entry_data.client.authenticate(entry_data.client.auth.access_token)
auth = await entry_data.client.get_access_token()
entry_data.client.nest_session = await entry_data.client.authenticate(auth)

# Subscribe to Google Nest subscribe endpoint
result = await entry_data.client.subscribe_for_data(
Expand Down
55 changes: 27 additions & 28 deletions custom_components/nest_protect/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,121 +38,121 @@ class NestProtectBinarySensorDescription(
key="co_status",
name="CO Status",
device_class=BinarySensorDeviceClass.CO,
value_fn=lambda state: state == 3,
value_fn=lambda state: state != 0,
),
NestProtectBinarySensorDescription(
key="smoke_status",
name="Smoke Status",
device_class=BinarySensorDeviceClass.SMOKE,
value_fn=lambda state: state == 3,
value_fn=lambda state: state != 0,
),
NestProtectBinarySensorDescription(
key="heat_status",
name="Heat Status",
device_class=BinarySensorDeviceClass.HEAT,
value_fn=lambda state: state == 3,
value_fn=lambda state: state != 0,
),
NestProtectBinarySensorDescription(
key="component_speaker_test_passed",
name="Speaker Test",
value_fn=lambda state: not state,
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
icon="mdi:speaker-wireless",
value_fn=lambda state: not state,
),
NestProtectBinarySensorDescription(
key="battery_health_state",
name="Battery Health",
value_fn=lambda state: state,
device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda state: state,
),
NestProtectBinarySensorDescription(
key="component_wifi_test_passed",
key="is_online",
name="Online",
value_fn=lambda state: state,
device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda state: state,
),
NestProtectBinarySensorDescription(
name="Smoke Test",
key="component_smoke_test_passed",
value_fn=lambda state: not state,
name="Smoke Test",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
icon="mdi:smoke",
value_fn=lambda state: not state,
),
NestProtectBinarySensorDescription(
name="CO Test",
key="component_co_test_passed",
value_fn=lambda state: not state,
name="CO Test",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
icon="mdi:molecule-co",
value_fn=lambda state: not state,
),
NestProtectBinarySensorDescription(
name="WiFi Test",
key="component_wifi_test_passed",
value_fn=lambda state: not state,
name="WiFi Test",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
icon="mdi:wifi",
value_fn=lambda state: not state,
),
NestProtectBinarySensorDescription(
name="LED Test",
key="component_led_test_passed",
value_fn=lambda state: not state,
name="LED Test",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
icon="mdi:led-off",
value_fn=lambda state: not state,
),
NestProtectBinarySensorDescription(
name="PIR Test",
key="component_pir_test_passed",
value_fn=lambda state: not state,
name="PIR Test",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
icon="mdi:run",
value_fn=lambda state: not state,
),
NestProtectBinarySensorDescription(
name="Buzzer Test",
key="component_buzzer_test_passed",
value_fn=lambda state: not state,
name="Buzzer Test",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
icon="mdi:alarm-bell",
value_fn=lambda state: not state,
),
# Disabled for now, since it seems like this state is not valid
# NestProtectBinarySensorDescription(
# name="Heat Test",
# key="component_heat_test_passed",
# value_fn=lambda state: not state,
# name="Heat Test",
# device_class=BinarySensorDeviceClass.PROBLEM,
# entity_category=EntityCategory.DIAGNOSTIC,
# icon="mdi:fire",
# value_fn=lambda state: not state
# ),
NestProtectBinarySensorDescription(
name="Humidity Test",
key="component_hum_test_passed",
value_fn=lambda state: not state,
name="Humidity Test",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
icon="mdi:water-percent",
value_fn=lambda state: not state,
),
NestProtectBinarySensorDescription(
name="Occupancy",
key="auto_away",
value_fn=lambda state: not state,
name="Occupancy",
device_class=BinarySensorDeviceClass.OCCUPANCY,
wired_only=True,
value_fn=lambda state: not state,
),
NestProtectBinarySensorDescription(
name="Line Power",
key="line_power_present",
value_fn=lambda state: state,
name="Line Power",
device_class=BinarySensorDeviceClass.POWER,
entity_category=EntityCategory.DIAGNOSTIC,
wired_only=True,
value_fn=lambda state: state,
),
]

Expand All @@ -170,7 +170,6 @@ async def async_setup_entry(hass, entry, async_add_devices):
for device in data.devices.values():
for key in device.value:
if description := SUPPORTED_KEYS.get(key):

# Not all entities are useful for battery powered Nest Protect devices
if description.wired_only and device.value["wired_or_battery"] != 0:
continue
Expand Down
Loading

0 comments on commit 4b3f2cc

Please sign in to comment.