Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use cookie for authentication #167

Merged
merged 28 commits into from
Apr 26, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1e62797
Use cookie for authentication
nicoinch Nov 17, 2022
caf7e9e
Merged changes from nicoinch
Cruguah Feb 19, 2023
ff77883
Merge branch 'nicoinch-main'
Cruguah Feb 19, 2023
1e3ecc7
Resolved some warnings
Cruguah Feb 22, 2023
c656727
Resolved a spelling error
Cruguah Feb 22, 2023
1d7eae2
Update pre-commit.yml
Cruguah Feb 22, 2023
8188f90
Update pre-commit.yml
Cruguah Feb 22, 2023
5a5f8a1
Update pre-commit.yml
Cruguah Feb 22, 2023
1219049
Update pre-commit.yml
Cruguah Feb 22, 2023
4c0603b
Resolve lint issues
Cruguah Feb 22, 2023
c9c29d5
Merge branch 'main' of https://github.com/Cruguah/ha-nest-protect
Cruguah Feb 22, 2023
087d19a
Update pre-commit.yml
Cruguah Feb 22, 2023
52a1f40
processed review comments
Cruguah Feb 25, 2023
ffd9bff
Merge branch 'main' of https://github.com/Cruguah/ha-nest-protect
Cruguah Feb 25, 2023
fbf9865
Resolved a validation issue
Cruguah Mar 8, 2023
3d83a84
Remove the version number
Cruguah Mar 12, 2023
7ecb075
Added the version tag again
Cruguah Mar 12, 2023
49657a7
Solved multiple issues
Cruguah Mar 27, 2023
eac9513
Resolve lint issues
Cruguah Mar 27, 2023
18e0214
Added reconfiguration to the nest integration
Cruguah Apr 5, 2023
e7ac358
Resolve pre-commit issue
Cruguah Apr 25, 2023
76ba32f
Try to solve the partial upgrade of isort config
Cruguah Apr 26, 2023
94f3a82
Try to resolve pre-check errors
Cruguah Apr 26, 2023
4cfcc16
Merge branch 'beta' into main
iMicknl Apr 26, 2023
ffe7a25
Resolve a conflict
Cruguah Apr 26, 2023
1e19d6e
Merge branch 'main' of https://github.com/Cruguah/ha-nest-protect
Cruguah Apr 26, 2023
9798b60
Revert changes
iMicknl Apr 26, 2023
7221d01
Update settings.json
iMicknl Apr 26, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pre-commit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Linters (flake8, black, isort)

on:
pull_request:

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

jobs:
lint:
runs-on: ubuntu-latest
Expand Down
37 changes: 29 additions & 8 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
{
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you revert the changes here? They don't seem necessary for this PR.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

"python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"python.pythonPath": "/usr/local/bin/python",
"files.associations": {
"*.yaml": "home-assistant"
},
"python.formatting.provider": "black"
}
"python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"python.pythonPath": "/usr/local/bin/python",
"files.associations": {
"*.yaml": "home-assistant"
},
"python.formatting.provider": "black",
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#fcc15a",
"activityBar.background": "#fcc15a",
"activityBar.foreground": "#15202b",
"activityBar.inactiveForeground": "#15202b99",
"activityBarBadge.background": "#039d65",
"activityBarBadge.foreground": "#e7e7e7",
"commandCenter.border": "#15202b99",
"sash.hoverBorder": "#fcc15a",
"statusBar.background": "#fbae28",
"statusBar.foreground": "#15202b",
"statusBarItem.hoverBackground": "#ec9704",
"statusBarItem.remoteBackground": "#fbae28",
"statusBarItem.remoteForeground": "#15202b",
"titleBar.activeBackground": "#fbae28",
"titleBar.activeForeground": "#15202b",
"titleBar.inactiveBackground": "#fbae2899",
"titleBar.inactiveForeground": "#15202b99"
},
"peacock.color": "#fbae28",
"githubPullRequests.ignoredPullRequestBranches": ["main"]
}
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.
iMicknl marked this conversation as resolved.
Show resolved Hide resolved

## 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
8 changes: 4 additions & 4 deletions custom_components/nest_protect/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,19 @@ class NestProtectBinarySensorDescription(
key="co_status",
name="CO Status",
device_class=BinarySensorDeviceClass.CO,
value_fn=lambda state: state == 3,
value_fn=lambda state: state != 0,
Cruguah marked this conversation as resolved.
Show resolved Hide resolved
),
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",
Expand All @@ -68,7 +68,7 @@ class NestProtectBinarySensorDescription(
entity_category=EntityCategory.DIAGNOSTIC,
),
NestProtectBinarySensorDescription(
key="component_wifi_test_passed",
key="is_online",
name="Online",
value_fn=lambda state: state,
device_class=BinarySensorDeviceClass.CONNECTIVITY,
Expand Down
47 changes: 31 additions & 16 deletions custom_components/nest_protect/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,18 @@
from aiohttp import ClientError
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_TOKEN, CONF_URL
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import voluptuous as vol

from .const import CONF_ACCOUNT_TYPE, CONF_REFRESH_TOKEN, DOMAIN, LOGGER
from .const import (
CONF_ACCOUNT_TYPE,
CONF_COOKIES,
CONF_ISSUE_TOKEN,
CONF_REFRESH_TOKEN,
DOMAIN,
LOGGER,
)
from .pynest.client import NestClient
from .pynest.const import NEST_ENVIRONMENTS
from .pynest.exceptions import BadCredentialsException
Expand All @@ -20,7 +26,7 @@
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for Nest Protect."""

VERSION = 2
VERSION = 3

_config_entry: ConfigEntry | None

Expand All @@ -37,19 +43,30 @@ async def async_validate_input(self, user_input: dict[str, Any]) -> None:
environment = user_input[CONF_ACCOUNT_TYPE]
session = async_get_clientsession(self.hass)
client = NestClient(session=session, environment=NEST_ENVIRONMENTS[environment])
token = user_input[CONF_TOKEN]

refresh_token = await client.get_refresh_token(token)
auth = await client.get_access_token(refresh_token)
if CONF_ISSUE_TOKEN in user_input and CONF_COOKIES in user_input:
issue_token = user_input[CONF_ISSUE_TOKEN]
cookies = user_input[CONF_COOKIES]
if CONF_REFRESH_TOKEN in user_input:
refresh_token = user_input[CONF_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"
)

await client.authenticate(
auth.access_token
) # TODO use result to gather more details

# TODO change unique id to an id related to the nest account
await self.async_set_unique_id(user_input[CONF_TOKEN])
await self.async_set_unique_id(user_input[CONF_ISSUE_TOKEN])

return refresh_token
return [issue_token, cookies]

async def async_step_user(
self, user_input: dict[str, Any] | None = None
Expand Down Expand Up @@ -84,8 +101,9 @@ async def async_step_account_link(
if user_input:
try:
user_input[CONF_ACCOUNT_TYPE] = self._default_account_type
refresh_token = await self.async_validate_input(user_input)
user_input[CONF_REFRESH_TOKEN] = refresh_token
[issue_token, cookies] = await self.async_validate_input(user_input)
user_input[CONF_ISSUE_TOKEN] = issue_token
user_input[CONF_COOKIES] = cookies
except (TimeoutError, ClientError):
errors["base"] = "cannot_connect"
except BadCredentialsException:
Expand Down Expand Up @@ -119,12 +137,9 @@ async def async_step_account_link(

return self.async_show_form(
step_id="account_link",
description_placeholders={
CONF_URL: NestClient.generate_token_url(
environment=NEST_ENVIRONMENTS[self._default_account_type]
)
},
data_schema=vol.Schema({vol.Required(CONF_TOKEN): str}),
data_schema=vol.Schema(
{vol.Required(CONF_ISSUE_TOKEN): str, vol.Required(CONF_COOKIES): str}
),
errors=errors,
)

Expand Down
2 changes: 2 additions & 0 deletions custom_components/nest_protect/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

CONF_ACCOUNT_TYPE: Final = "account_type"
CONF_REFRESH_TOKEN: Final = "refresh_token"
CONF_ISSUE_TOKEN: Final = "issue_token"
CONF_COOKIES: Final = "cookies"

PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Expand Down
30 changes: 26 additions & 4 deletions custom_components/nest_protect/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from homeassistant.helpers.device_registry import DeviceEntry

from . import HomeAssistantNestProtectData
from .const import CONF_REFRESH_TOKEN, DOMAIN
from .const import CONF_COOKIES, CONF_ISSUE_TOKEN, CONF_REFRESH_TOKEN, DOMAIN

TO_REDACT = [
"access_token",
Expand Down Expand Up @@ -46,12 +46,25 @@ async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
refresh_token = entry.data[CONF_REFRESH_TOKEN]

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]

entry_data: HomeAssistantNestProtectData = hass.data[DOMAIN][entry.entry_id]
client = entry_data.client

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)

data = {"app_launch": await client.get_first_data(nest.access_token, nest.userid)}
Expand All @@ -64,11 +77,20 @@ async def async_get_device_diagnostics(
) -> dict[str, Any]:
"""Return diagnostics for a device entry."""
refresh_token = entry.data[CONF_REFRESH_TOKEN]
issue_token = entry.data[CONF_ISSUE_TOKEN]
cookies = entry.data[CONF_COOKIES]

entry_data: HomeAssistantNestProtectData = hass.data[DOMAIN][entry.entry_id]
client = entry_data.client

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)

data = {
Expand Down
1 change: 1 addition & 0 deletions custom_components/nest_protect/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ async def async_added_to_hass(self) -> None:
@callback
def update_callback(self, bucket: Bucket):
"""Update the entities state."""

self.bucket = bucket
self.async_write_ha_state()

Expand Down
32 changes: 15 additions & 17 deletions custom_components/nest_protect/manifest.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
{
"domain": "nest_protect",
iMicknl marked this conversation as resolved.
Show resolved Hide resolved
"name": "Nest Protect",
"config_flow": true,
"documentation": "https://github.com/imicknl/ha-nest-protect",
"issue_tracker": "https://github.com/imicknl/ha-nest-protect/issues",
"requirements": [],
"codeowners": [
"@imicknl"
],
"iot_class": "cloud_polling",
"version": "0.3.8",
"dhcp": [
{
"macaddress": "CCA7C1*"
}
]
}
"domain": "nest_protect",
"name": "Nest Protect",
"config_flow": true,
"documentation": "https://github.com/imicknl/ha-nest-protect",
"issue_tracker": "https://github.com/imicknl/ha-nest-protect/issues",
"requirements": [],
"codeowners": ["@imicknl"],
"iot_class": "cloud_polling",
"version": "0.4.0",
Cruguah marked this conversation as resolved.
Show resolved Hide resolved
"dhcp": [
{
"macaddress": "CCA7C1*"
}
]
}
Loading