mirror of
https://github.com/USA-RedDragon/badnest.git
synced 2025-04-27 15:24:35 +01:00
Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
faa0c956db | ||
|
7ab48d968e | ||
|
59bb5ae237 | ||
|
d74dc3f2ea | ||
|
a5b57f0cf0 | ||
|
1069805ed8 | ||
|
857463f3d4 | ||
|
cbc8702ee0 | ||
|
f1ebd8a2f7 | ||
|
2e9378de06 | ||
|
5b9cf52aff | ||
|
5b3c70e0ec | ||
|
63da0e61b0 | ||
|
e55159b465 | ||
|
933b7a017b | ||
|
de8dfa1490 | ||
|
222f981692 |
@ -1,17 +1,16 @@
|
||||
"""The example integration."""
|
||||
import voluptuous as vol
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||
|
||||
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(
|
||||
{
|
||||
DOMAIN: vol.All(
|
||||
{
|
||||
vol.Required(CONF_EMAIL, default=""): cv.string,
|
||||
vol.Required(CONF_PASSWORD, default=""): cv.string,
|
||||
vol.Required(CONF_USER_ID, default=""): cv.string,
|
||||
vol.Required(CONF_ACCESS_TOKEN, default=""): cv.string,
|
||||
vol.Optional(CONF_REGION, default="us"): cv.string,
|
||||
},
|
||||
{
|
||||
@ -28,8 +27,8 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
def setup(hass, config):
|
||||
"""Set up the badnest component."""
|
||||
if config.get(DOMAIN) is not None:
|
||||
email = config[DOMAIN].get(CONF_EMAIL)
|
||||
password = config[DOMAIN].get(CONF_PASSWORD)
|
||||
user_id = config[DOMAIN].get(CONF_USER_ID)
|
||||
access_token = config[DOMAIN].get(CONF_ACCESS_TOKEN)
|
||||
issue_token = config[DOMAIN].get(CONF_ISSUE_TOKEN)
|
||||
cookie = config[DOMAIN].get(CONF_COOKIE)
|
||||
region = config[DOMAIN].get(CONF_REGION)
|
||||
@ -42,8 +41,8 @@ def setup(hass, config):
|
||||
|
||||
hass.data[DOMAIN] = {
|
||||
'api': NestAPI(
|
||||
email,
|
||||
password,
|
||||
user_id,
|
||||
access_token,
|
||||
issue_token,
|
||||
cookie,
|
||||
region,
|
||||
|
@ -27,22 +27,20 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
class NestAPI():
|
||||
def __init__(self,
|
||||
email,
|
||||
password,
|
||||
user_id,
|
||||
access_token,
|
||||
issue_token,
|
||||
cookie,
|
||||
region):
|
||||
self.device_data = {}
|
||||
self._wheres = {}
|
||||
self._user_id = None
|
||||
self._access_token = None
|
||||
self._user_id = user_id
|
||||
self._access_token = access_token
|
||||
self._session = requests.Session()
|
||||
self._session.headers.update({
|
||||
"Referer": "https://home.nest.com/",
|
||||
"User-Agent": USER_AGENT,
|
||||
})
|
||||
self._email = email
|
||||
self._password = password
|
||||
self._issue_token = issue_token
|
||||
self._cookie = cookie
|
||||
self._czfe_url = None
|
||||
@ -68,19 +66,10 @@ class NestAPI():
|
||||
return hasattr(self, name)
|
||||
|
||||
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)
|
||||
else:
|
||||
self._login_nest(self._email, self._password)
|
||||
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):
|
||||
headers = {
|
||||
'User-Agent': USER_AGENT,
|
||||
@ -163,6 +152,7 @@ class NestAPI():
|
||||
elif bucket.startswith('device.'):
|
||||
sn = bucket.replace('device.', '')
|
||||
self.thermostats.append(sn)
|
||||
self.temperature_sensors.append(sn)
|
||||
self.device_data[sn] = {}
|
||||
|
||||
self.cameras = self._get_cameras()
|
||||
@ -176,6 +166,17 @@ class NestAPI():
|
||||
self.login()
|
||||
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):
|
||||
try:
|
||||
# To get friendly names
|
||||
@ -209,7 +210,7 @@ class NestAPI():
|
||||
for bucket in r.json()["updated_buckets"]:
|
||||
sensor_data = bucket["value"]
|
||||
sn = bucket["object_key"].split('.')[1]
|
||||
# Thermostats
|
||||
# Thermostats (thermostat and sensors system)
|
||||
if bucket["object_key"].startswith(
|
||||
f"shared.{sn}"):
|
||||
self.device_data[sn]['current_temperature'] = \
|
||||
@ -242,6 +243,14 @@ class NestAPI():
|
||||
self.device_data[sn]['name'] = self._wheres[
|
||||
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):
|
||||
self.device_data[sn]['name'] += \
|
||||
f' ({sensor_data["description"]})'
|
||||
@ -252,6 +261,10 @@ class NestAPI():
|
||||
sensor_data["fan_timer_timeout"]
|
||||
self.device_data[sn]['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 \
|
||||
sensor_data["eco"]["mode"] == 'auto-eco':
|
||||
self.device_data[sn]['eco'] = True
|
||||
@ -268,11 +281,11 @@ class NestAPI():
|
||||
f' ({sensor_data["description"]})'
|
||||
self.device_data[sn]['name'] += ' Protect'
|
||||
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'] = \
|
||||
sensor_data['smoke_status']
|
||||
self._map_nest_protect_state(sensor_data['smoke_status'])
|
||||
self.device_data[sn]['battery_health_state'] = \
|
||||
sensor_data['battery_health_state']
|
||||
self._map_nest_protect_state(sensor_data['battery_health_state'])
|
||||
# Temperature sensors
|
||||
elif bucket["object_key"].startswith(
|
||||
f"kryptonite.{sn}"):
|
||||
@ -362,6 +375,33 @@ class NestAPI():
|
||||
self.login()
|
||||
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):
|
||||
if device_id not in self.thermostats:
|
||||
return
|
||||
|
@ -65,7 +65,7 @@ class NestCamera(Camera):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if on."""
|
||||
return self._device.device_data[self._uuid]['online']
|
||||
return self._device.device_data[self._uuid]['is_online']
|
||||
|
||||
@property
|
||||
def is_recording(self):
|
||||
|
@ -16,6 +16,7 @@ from homeassistant.components.climate.const import (
|
||||
SUPPORT_PRESET_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_TARGET_TEMPERATURE_RANGE,
|
||||
SUPPORT_TARGET_HUMIDITY,
|
||||
PRESET_ECO,
|
||||
PRESET_NONE,
|
||||
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()}
|
||||
|
||||
ROUND_TARGET_HUMIDITY_TO_NEAREST = 5
|
||||
NEST_HUMIDITY_MIN = 10
|
||||
NEST_HUMIDITY_MAX = 60
|
||||
|
||||
PRESET_MODES = [PRESET_NONE, PRESET_ECO]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -109,6 +114,10 @@ class NestClimate(ClimateDevice):
|
||||
if self.device.device_data[device_id]['has_fan']:
|
||||
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
|
||||
def unique_id(self):
|
||||
"""Return an unique ID."""
|
||||
@ -144,6 +153,21 @@ class NestClimate(ClimateDevice):
|
||||
"""Return the 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
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
@ -253,6 +277,18 @@ class NestClimate(ClimateDevice):
|
||||
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):
|
||||
"""Set operation mode."""
|
||||
self.device.thermostat_set_mode(
|
||||
|
@ -1,4 +1,6 @@
|
||||
DOMAIN = 'badnest'
|
||||
CONF_ISSUE_TOKEN = 'issue_token'
|
||||
CONF_COOKIE = 'cookie'
|
||||
CONF_USER_ID = 'user_id'
|
||||
CONF_ACCESS_TOKEN = 'access_token'
|
||||
CONF_REGION = 'region'
|
||||
|
28
info.md
28
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
|
||||
|
||||
A bad Nest thermostat and camera 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
|
||||
|
||||
@ -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
|
||||
- Works with migrated/new accounts via Google auth
|
||||
- Works with old via Nest auth
|
||||
- Nest Protect support
|
||||
- Nest Thermostat 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
|
||||
|
||||
- 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
|
||||
- Camera 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
|
||||
|
||||
@ -32,10 +32,17 @@ two-character country code, and it should work.
|
||||
|
||||
### 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
|
||||
badnest:
|
||||
email: email@domain.com
|
||||
password: !secret nest_password
|
||||
user_id: 11111
|
||||
access_token: !secret nest_access_token
|
||||
region: us
|
||||
|
||||
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`
|
||||
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`.
|
||||
|
||||
## 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