mirror of
https://github.com/USA-RedDragon/badnest.git
synced 2025-04-28 01:54:33 +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."""
|
||||
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'
|
||||
|
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
|
||||
|
||||
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