mirror of
https://github.com/USA-RedDragon/badnest.git
synced 2025-05-24 19:15:48 +01:00
Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
faa0c956db | ||
|
7ab48d968e | ||
|
59bb5ae237 | ||
|
d74dc3f2ea | ||
|
a5b57f0cf0 | ||
|
1069805ed8 | ||
|
857463f3d4 | ||
|
cbc8702ee0 | ||
|
f1ebd8a2f7 | ||
|
2e9378de06 | ||
|
5b9cf52aff | ||
|
5b3c70e0ec | ||
|
63da0e61b0 | ||
|
e55159b465 | ||
|
933b7a017b |
@ -1,17 +1,16 @@
|
|||||||
"""The example integration."""
|
"""The example integration."""
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
|
||||||
|
|
||||||
from .api import NestAPI
|
from .api import NestAPI
|
||||||
from .const import DOMAIN, CONF_ISSUE_TOKEN, CONF_COOKIE, CONF_REGION
|
from .const import DOMAIN, CONF_ISSUE_TOKEN, CONF_COOKIE, CONF_USER_ID, CONF_ACCESS_TOKEN, CONF_REGION
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
DOMAIN: vol.All(
|
DOMAIN: vol.All(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_EMAIL, default=""): cv.string,
|
vol.Required(CONF_USER_ID, default=""): cv.string,
|
||||||
vol.Required(CONF_PASSWORD, default=""): cv.string,
|
vol.Required(CONF_ACCESS_TOKEN, default=""): cv.string,
|
||||||
vol.Optional(CONF_REGION, default="us"): cv.string,
|
vol.Optional(CONF_REGION, default="us"): cv.string,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -28,8 +27,8 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Set up the badnest component."""
|
"""Set up the badnest component."""
|
||||||
if config.get(DOMAIN) is not None:
|
if config.get(DOMAIN) is not None:
|
||||||
email = config[DOMAIN].get(CONF_EMAIL)
|
user_id = config[DOMAIN].get(CONF_USER_ID)
|
||||||
password = config[DOMAIN].get(CONF_PASSWORD)
|
access_token = config[DOMAIN].get(CONF_ACCESS_TOKEN)
|
||||||
issue_token = config[DOMAIN].get(CONF_ISSUE_TOKEN)
|
issue_token = config[DOMAIN].get(CONF_ISSUE_TOKEN)
|
||||||
cookie = config[DOMAIN].get(CONF_COOKIE)
|
cookie = config[DOMAIN].get(CONF_COOKIE)
|
||||||
region = config[DOMAIN].get(CONF_REGION)
|
region = config[DOMAIN].get(CONF_REGION)
|
||||||
@ -42,8 +41,8 @@ def setup(hass, config):
|
|||||||
|
|
||||||
hass.data[DOMAIN] = {
|
hass.data[DOMAIN] = {
|
||||||
'api': NestAPI(
|
'api': NestAPI(
|
||||||
email,
|
user_id,
|
||||||
password,
|
access_token,
|
||||||
issue_token,
|
issue_token,
|
||||||
cookie,
|
cookie,
|
||||||
region,
|
region,
|
||||||
|
@ -27,22 +27,20 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class NestAPI():
|
class NestAPI():
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
email,
|
user_id,
|
||||||
password,
|
access_token,
|
||||||
issue_token,
|
issue_token,
|
||||||
cookie,
|
cookie,
|
||||||
region):
|
region):
|
||||||
self.device_data = {}
|
self.device_data = {}
|
||||||
self._wheres = {}
|
self._wheres = {}
|
||||||
self._user_id = None
|
self._user_id = user_id
|
||||||
self._access_token = None
|
self._access_token = access_token
|
||||||
self._session = requests.Session()
|
self._session = requests.Session()
|
||||||
self._session.headers.update({
|
self._session.headers.update({
|
||||||
"Referer": "https://home.nest.com/",
|
"Referer": "https://home.nest.com/",
|
||||||
"User-Agent": USER_AGENT,
|
"User-Agent": USER_AGENT,
|
||||||
})
|
})
|
||||||
self._email = email
|
|
||||||
self._password = password
|
|
||||||
self._issue_token = issue_token
|
self._issue_token = issue_token
|
||||||
self._cookie = cookie
|
self._cookie = cookie
|
||||||
self._czfe_url = None
|
self._czfe_url = None
|
||||||
@ -68,19 +66,10 @@ class NestAPI():
|
|||||||
return hasattr(self, name)
|
return hasattr(self, name)
|
||||||
|
|
||||||
def login(self):
|
def login(self):
|
||||||
if not self._email and not self._password:
|
if self._issue_token and self._cookie:
|
||||||
self._login_google(self._issue_token, self._cookie)
|
self._login_google(self._issue_token, self._cookie)
|
||||||
else:
|
|
||||||
self._login_nest(self._email, self._password)
|
|
||||||
self._login_dropcam()
|
self._login_dropcam()
|
||||||
|
|
||||||
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):
|
def _login_google(self, issue_token, cookie):
|
||||||
headers = {
|
headers = {
|
||||||
'User-Agent': USER_AGENT,
|
'User-Agent': USER_AGENT,
|
||||||
@ -163,6 +152,7 @@ class NestAPI():
|
|||||||
elif bucket.startswith('device.'):
|
elif bucket.startswith('device.'):
|
||||||
sn = bucket.replace('device.', '')
|
sn = bucket.replace('device.', '')
|
||||||
self.thermostats.append(sn)
|
self.thermostats.append(sn)
|
||||||
|
self.temperature_sensors.append(sn)
|
||||||
self.device_data[sn] = {}
|
self.device_data[sn] = {}
|
||||||
|
|
||||||
self.cameras = self._get_cameras()
|
self.cameras = self._get_cameras()
|
||||||
@ -176,6 +166,17 @@ class NestAPI():
|
|||||||
self.login()
|
self.login()
|
||||||
return self.get_devices()
|
return self.get_devices()
|
||||||
|
|
||||||
|
|
||||||
|
def _map_nest_protect_state(self, value):
|
||||||
|
if value == 0:
|
||||||
|
return "Ok"
|
||||||
|
elif value == 1 or value == 2:
|
||||||
|
return "Warning"
|
||||||
|
elif value == 3:
|
||||||
|
return "Emergency"
|
||||||
|
else:
|
||||||
|
return "Unknown"
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
try:
|
try:
|
||||||
# To get friendly names
|
# To get friendly names
|
||||||
@ -209,7 +210,7 @@ class NestAPI():
|
|||||||
for bucket in r.json()["updated_buckets"]:
|
for bucket in r.json()["updated_buckets"]:
|
||||||
sensor_data = bucket["value"]
|
sensor_data = bucket["value"]
|
||||||
sn = bucket["object_key"].split('.')[1]
|
sn = bucket["object_key"].split('.')[1]
|
||||||
# Thermostats
|
# Thermostats (thermostat and sensors system)
|
||||||
if bucket["object_key"].startswith(
|
if bucket["object_key"].startswith(
|
||||||
f"shared.{sn}"):
|
f"shared.{sn}"):
|
||||||
self.device_data[sn]['current_temperature'] = \
|
self.device_data[sn]['current_temperature'] = \
|
||||||
@ -242,6 +243,14 @@ class NestAPI():
|
|||||||
self.device_data[sn]['name'] = self._wheres[
|
self.device_data[sn]['name'] = self._wheres[
|
||||||
sensor_data['where_id']
|
sensor_data['where_id']
|
||||||
]
|
]
|
||||||
|
# When acts as a sensor
|
||||||
|
if 'backplate_temperature' in sensor_data:
|
||||||
|
self.device_data[sn]['temperature'] = \
|
||||||
|
sensor_data['backplate_temperature']
|
||||||
|
if 'battery_level' in sensor_data:
|
||||||
|
self.device_data[sn]['battery_level'] = \
|
||||||
|
sensor_data['battery_level']
|
||||||
|
|
||||||
if sensor_data.get('description', None):
|
if sensor_data.get('description', None):
|
||||||
self.device_data[sn]['name'] += \
|
self.device_data[sn]['name'] += \
|
||||||
f' ({sensor_data["description"]})'
|
f' ({sensor_data["description"]})'
|
||||||
@ -252,6 +261,10 @@ class NestAPI():
|
|||||||
sensor_data["fan_timer_timeout"]
|
sensor_data["fan_timer_timeout"]
|
||||||
self.device_data[sn]['current_humidity'] = \
|
self.device_data[sn]['current_humidity'] = \
|
||||||
sensor_data["current_humidity"]
|
sensor_data["current_humidity"]
|
||||||
|
self.device_data[sn]['target_humidity'] = \
|
||||||
|
sensor_data["target_humidity"]
|
||||||
|
self.device_data[sn]['target_humidity_enabled'] = \
|
||||||
|
sensor_data["target_humidity_enabled"]
|
||||||
if sensor_data["eco"]["mode"] == 'manual-eco' or \
|
if sensor_data["eco"]["mode"] == 'manual-eco' or \
|
||||||
sensor_data["eco"]["mode"] == 'auto-eco':
|
sensor_data["eco"]["mode"] == 'auto-eco':
|
||||||
self.device_data[sn]['eco'] = True
|
self.device_data[sn]['eco'] = True
|
||||||
@ -268,11 +281,11 @@ class NestAPI():
|
|||||||
f' ({sensor_data["description"]})'
|
f' ({sensor_data["description"]})'
|
||||||
self.device_data[sn]['name'] += ' Protect'
|
self.device_data[sn]['name'] += ' Protect'
|
||||||
self.device_data[sn]['co_status'] = \
|
self.device_data[sn]['co_status'] = \
|
||||||
sensor_data['co_status']
|
self._map_nest_protect_state(sensor_data['co_status'])
|
||||||
self.device_data[sn]['smoke_status'] = \
|
self.device_data[sn]['smoke_status'] = \
|
||||||
sensor_data['smoke_status']
|
self._map_nest_protect_state(sensor_data['smoke_status'])
|
||||||
self.device_data[sn]['battery_health_state'] = \
|
self.device_data[sn]['battery_health_state'] = \
|
||||||
sensor_data['battery_health_state']
|
self._map_nest_protect_state(sensor_data['battery_health_state'])
|
||||||
# Temperature sensors
|
# Temperature sensors
|
||||||
elif bucket["object_key"].startswith(
|
elif bucket["object_key"].startswith(
|
||||||
f"kryptonite.{sn}"):
|
f"kryptonite.{sn}"):
|
||||||
@ -362,6 +375,33 @@ class NestAPI():
|
|||||||
self.login()
|
self.login()
|
||||||
self.thermostat_set_temperature(device_id, temp, temp_high)
|
self.thermostat_set_temperature(device_id, temp, temp_high)
|
||||||
|
|
||||||
|
def thermostat_set_target_humidity(self, device_id, humidity):
|
||||||
|
if device_id not in self.thermostats:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._session.post(
|
||||||
|
f"{self._czfe_url}/v5/put",
|
||||||
|
json={
|
||||||
|
"objects": [
|
||||||
|
{
|
||||||
|
"object_key": f'device.{device_id}',
|
||||||
|
"op": "MERGE",
|
||||||
|
"value": {"target_humidity": humidity},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
headers={"Authorization": f"Basic {self._access_token}"},
|
||||||
|
)
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
_LOGGER.error(e)
|
||||||
|
_LOGGER.error('Failed to set humidity, trying again')
|
||||||
|
self.thermostat_set_target_humidity(device_id, humidity)
|
||||||
|
except KeyError:
|
||||||
|
_LOGGER.debug('Failed to set humidity, trying to log in again')
|
||||||
|
self.login()
|
||||||
|
self.thermostat_set_target_humidity(device_id, humidity)
|
||||||
|
|
||||||
def thermostat_set_mode(self, device_id, mode):
|
def thermostat_set_mode(self, device_id, mode):
|
||||||
if device_id not in self.thermostats:
|
if device_id not in self.thermostats:
|
||||||
return
|
return
|
||||||
|
@ -65,7 +65,7 @@ class NestCamera(Camera):
|
|||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
"""Return true if on."""
|
"""Return true if on."""
|
||||||
return self._device.device_data[self._uuid]['online']
|
return self._device.device_data[self._uuid]['is_online']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_recording(self):
|
def is_recording(self):
|
||||||
|
@ -16,6 +16,7 @@ from homeassistant.components.climate.const import (
|
|||||||
SUPPORT_PRESET_MODE,
|
SUPPORT_PRESET_MODE,
|
||||||
SUPPORT_TARGET_TEMPERATURE,
|
SUPPORT_TARGET_TEMPERATURE,
|
||||||
SUPPORT_TARGET_TEMPERATURE_RANGE,
|
SUPPORT_TARGET_TEMPERATURE_RANGE,
|
||||||
|
SUPPORT_TARGET_HUMIDITY,
|
||||||
PRESET_ECO,
|
PRESET_ECO,
|
||||||
PRESET_NONE,
|
PRESET_NONE,
|
||||||
CURRENT_HVAC_HEAT,
|
CURRENT_HVAC_HEAT,
|
||||||
@ -52,6 +53,10 @@ ACTION_NEST_TO_HASS = {
|
|||||||
|
|
||||||
MODE_NEST_TO_HASS = {v: k for k, v in MODE_HASS_TO_NEST.items()}
|
MODE_NEST_TO_HASS = {v: k for k, v in MODE_HASS_TO_NEST.items()}
|
||||||
|
|
||||||
|
ROUND_TARGET_HUMIDITY_TO_NEAREST = 5
|
||||||
|
NEST_HUMIDITY_MIN = 10
|
||||||
|
NEST_HUMIDITY_MAX = 60
|
||||||
|
|
||||||
PRESET_MODES = [PRESET_NONE, PRESET_ECO]
|
PRESET_MODES = [PRESET_NONE, PRESET_ECO]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -109,6 +114,10 @@ class NestClimate(ClimateDevice):
|
|||||||
if self.device.device_data[device_id]['has_fan']:
|
if self.device.device_data[device_id]['has_fan']:
|
||||||
self._support_flags = self._support_flags | SUPPORT_FAN_MODE
|
self._support_flags = self._support_flags | SUPPORT_FAN_MODE
|
||||||
|
|
||||||
|
if self.device.device_data[device_id]['target_humidity_enabled']:
|
||||||
|
self._support_flags = self._support_flags | SUPPORT_TARGET_HUMIDITY
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
"""Return an unique ID."""
|
"""Return an unique ID."""
|
||||||
@ -144,6 +153,21 @@ class NestClimate(ClimateDevice):
|
|||||||
"""Return the current humidity."""
|
"""Return the current humidity."""
|
||||||
return self.device.device_data[self.device_id]['current_humidity']
|
return self.device.device_data[self.device_id]['current_humidity']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_humidity(self):
|
||||||
|
"""Return the target humidity."""
|
||||||
|
return self.device.device_data[self.device_id]['target_humidity']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min_humidity(self):
|
||||||
|
"""Return the min target humidity."""
|
||||||
|
return NEST_HUMIDITY_MIN
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_humidity(self):
|
||||||
|
"""Return the max target humidity."""
|
||||||
|
return NEST_HUMIDITY_MAX
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_temperature(self):
|
def target_temperature(self):
|
||||||
"""Return the temperature we try to reach."""
|
"""Return the temperature we try to reach."""
|
||||||
@ -253,6 +277,18 @@ class NestClimate(ClimateDevice):
|
|||||||
temp,
|
temp,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def set_humidity(self, humidity):
|
||||||
|
"""Set new target humidity."""
|
||||||
|
humidity = int(round(float(humidity) / ROUND_TARGET_HUMIDITY_TO_NEAREST) * ROUND_TARGET_HUMIDITY_TO_NEAREST)
|
||||||
|
if humidity < NEST_HUMIDITY_MIN:
|
||||||
|
humidity = NEST_HUMIDITY_MIN
|
||||||
|
if humidity > NEST_HUMIDITY_MAX:
|
||||||
|
humidity = NEST_HUMIDITY_MAX
|
||||||
|
self.device.thermostat_set_target_humidity(
|
||||||
|
self.device_id,
|
||||||
|
humidity,
|
||||||
|
)
|
||||||
|
|
||||||
def set_hvac_mode(self, hvac_mode):
|
def set_hvac_mode(self, hvac_mode):
|
||||||
"""Set operation mode."""
|
"""Set operation mode."""
|
||||||
self.device.thermostat_set_mode(
|
self.device.thermostat_set_mode(
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
DOMAIN = 'badnest'
|
DOMAIN = 'badnest'
|
||||||
CONF_ISSUE_TOKEN = 'issue_token'
|
CONF_ISSUE_TOKEN = 'issue_token'
|
||||||
CONF_COOKIE = 'cookie'
|
CONF_COOKIE = 'cookie'
|
||||||
|
CONF_USER_ID = 'user_id'
|
||||||
|
CONF_ACCESS_TOKEN = 'access_token'
|
||||||
CONF_REGION = 'region'
|
CONF_REGION = 'region'
|
||||||
|
26
info.md
26
info.md
@ -1,8 +1,10 @@
|
|||||||
|
NOTICE: THIS PROJECT IS RETIRED NOW THAT GOOGLE HAS RELEASED THE SDM SMART DEVICE MANAGMENT API. FUTHER WORK CAN BE FOUND AT <https://github.com/USA-RedDragon/badnest-sdm> and <https://github.com/USA-RedDragon/python-google-sdm>.
|
||||||
|
|
||||||
# badnest
|
# badnest
|
||||||
|
|
||||||
A bad Nest integration that uses the web api to work after Works with Nest was shut down (bad Google, go sit in your corner and think about what you did)
|
A bad Nest integration that uses the web api to work after Works with Nest was shut down (bad Google, go sit in your corner and think about what you did)
|
||||||
|
|
||||||
## Why is it bad?
|
## Why is it bad
|
||||||
|
|
||||||
This isn't an advertised or public API, it's still better than web scraping, but will never be as reliable as the original API
|
This isn't an advertised or public API, it's still better than web scraping, but will never be as reliable as the original API
|
||||||
|
|
||||||
@ -10,7 +12,6 @@ This isn't an advertised or public API, it's still better than web scraping, but
|
|||||||
|
|
||||||
- Doesn't use the now defunct Works with Nest API
|
- Doesn't use the now defunct Works with Nest API
|
||||||
- Works with migrated/new accounts via Google auth
|
- Works with migrated/new accounts via Google auth
|
||||||
- Works with old via Nest auth
|
|
||||||
- Nest Protect support
|
- Nest Protect support
|
||||||
- Nest Thermostat support
|
- Nest Thermostat support
|
||||||
- Nest Thermostat Sensor support
|
- Nest Thermostat Sensor support
|
||||||
@ -18,11 +19,10 @@ This isn't an advertised or public API, it's still better than web scraping, but
|
|||||||
|
|
||||||
## Drawbacks
|
## Drawbacks
|
||||||
|
|
||||||
- Won't work with 2FA enabled accounts (Works with 2fa Google Accounts)
|
|
||||||
- Tested with a single thermostat, I have no other devices to test with
|
- Tested with a single thermostat, I have no other devices to test with
|
||||||
- Camera integration is untested by me
|
- Camera integration is untested by me
|
||||||
- Nest Protect integration is untested by me
|
- Nest Protect integration is untested by me
|
||||||
- Nest could change their webapp api at any time, making this defunct
|
- Nest could change their webapp api at any time, making this defunct (this has happened before, see <https://github.com/USA-RedDragon/badnest/issues/67>)
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@ -32,10 +32,17 @@ two-character country code, and it should work.
|
|||||||
|
|
||||||
### Example configuration.yaml - When you're not using the Google Auth Login
|
### Example configuration.yaml - When you're not using the Google Auth Login
|
||||||
|
|
||||||
|
Google recently introduced reCAPTCHA when logging to Nest. That means username
|
||||||
|
and password cannot be used directly any more. Instead, you have to obtain
|
||||||
|
`user_id` and `access_token` for your account by logging in manually. To do that,
|
||||||
|
open developer tools in your browser, switch to the "Network" tab, log in to Nest
|
||||||
|
and look for the request similar to `https://home.nest.com/session?_=1578693398448`.
|
||||||
|
You will find `user_id` and `access_token` in the response to the request.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
badnest:
|
badnest:
|
||||||
email: email@domain.com
|
user_id: 11111
|
||||||
password: !secret nest_password
|
access_token: !secret nest_access_token
|
||||||
region: us
|
region: us
|
||||||
|
|
||||||
climate:
|
climate:
|
||||||
@ -82,3 +89,10 @@ The values of `"issue_token"` and `"cookie"` are specific to your Google Account
|
|||||||
8. In the 'Filter' box, enter `oauth2/iframe`
|
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.
|
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`.
|
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`.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
The target temperature reported by the integration sometimes _seems_ to be slightly off by a few tens of a degree.
|
||||||
|
This is caused by the fact that the Nest mobile app actually actually allows users to set the temperature in small
|
||||||
|
increments, but the displayed temperature is rounded to the nearest 0.5 degree. In other words, the temperature
|
||||||
|
displayed by the integration is correct, just _more exact_ than what is shown in the app.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user