1
0
mirror of https://github.com/USA-RedDragon/badnest.git synced 2025-04-30 09:34:33 +01:00

Compare commits

...

41 Commits

Author SHA1 Message Date
Jacob McSwain
faa0c956db
Add retirement notice 2020-10-15 19:50:27 -05:00
Jacob McSwain
7ab48d968e Adapt @therealryanbonham's PR ()
This removes the formatting portion, which should have been a separate PR
2020-02-03 15:00:20 -06:00
Jacob McSwain
59bb5ae237 Update README 2020-02-03 14:47:43 -06:00
Jacob McSwain
d74dc3f2ea
Merge pull request from piit79/apparent-temp-offset
Update info.md with a note about seemingly off temperatures
2020-02-03 14:44:32 -06:00
Jacob McSwain
a5b57f0cf0
Merge pull request from piit79/nest-access-token
Switch from Nest username/password to user_id/access_token
2020-02-03 14:43:42 -06:00
Jacob McSwain
1069805ed8
Merge pull request from chrismarcellino/master
Add target humidity support to allow automation
2020-02-03 14:42:24 -06:00
Jacob McSwain
857463f3d4
Merge pull request from nonsleepr/thermostat-as-a-sensor
Expose thermostat as a sensor
2020-02-03 14:41:14 -06:00
Chris Marcellino
cbc8702ee0 Fix copy-past error in humidity setter code 2020-01-20 07:18:37 -06:00
Chris Marcellino
f1ebd8a2f7 Round humidity changes to nearest 5% instead of down 2020-01-19 15:09:22 -06:00
Petr Sedlacek
2e9378de06 Switch from Nest username/password to user_id/access_token
Username and password can no longer be used directly because of reCAPTCHA.
2020-01-14 15:25:48 +01:00
Petr Sedlacek
5b9cf52aff Update info.md with a note about seemingly off temperatures 2020-01-10 23:52:43 +01:00
Chris Marcellino
5b3c70e0ec Add target humidity support to allow automation 2019-12-13 16:42:47 -06:00
Alexander Bessonov
63da0e61b0 Expose thermostat as a sensor 2019-12-11 10:55:17 -05:00
Jacob McSwain
e55159b465
Merge pull request from USA-RedDragon/is_online-regression
Camera: Bugfix: online -> is_online
2019-11-04 09:16:04 -06:00
Jacob McSwain
933b7a017b
Camera: Bugfix: online -> is_online 2019-11-04 09:15:28 -06:00
Jacob McSwain
de8dfa1490
Merge pull request from USA-RedDragon/not-just-a-thermostat-pt-2
More changes to remove just a thermostat
2019-11-03 22:12:35 -06:00
Jacob McSwain
222f981692
More changes to remove just a thermostat 2019-11-03 22:12:12 -06:00
Jacob McSwain
cc7ec56d2a
Merge pull request from USA-RedDragon/not-just-a-thermostat
It's no longer just a thermostat
2019-11-03 22:09:50 -06:00
Jacob McSwain
385e97a62a
It's no longer just a thermostat 2019-11-03 22:09:20 -06:00
Jacob McSwain
fe6f04e645
Merge pull request from USA-RedDragon/readme-maintenance
Update README to reflect all the changes
2019-11-03 21:54:22 -06:00
Jacob McSwain
799d7a55dc
Update README to reflect all the changes 2019-11-03 21:53:31 -06:00
Jacob McSwain
0c87f7561a
Merge pull request from USA-RedDragon/nest_protect
Add preliminary Nest Protect support
2019-11-03 21:46:57 -06:00
Jacob McSwain
c9693df3a2
Merge pull request from USA-RedDragon/rewrite-api
Rewrite API to be a singleton
2019-11-03 21:45:22 -06:00
Jacob McSwain
2927881c33
Add preliminary Nest Protect support 2019-11-03 21:44:38 -06:00
Jacob McSwain
334ce8579f
Rewrite API to be a singleton
This should make error handling and extending this to more Nest platforms much easier
2019-11-03 21:43:45 -06:00
Jacob McSwain
98490b08f5
Merge pull request from USA-RedDragon/remove_api_key
Remove API Key from the config.
2019-11-03 17:29:12 -06:00
Jacob McSwain
7cef7c65a4
Remove API Key from the config.
Apparently this is public and shared, I was being overly protective.

Thanks to @gboudreau for this.

https://github.com/USA-RedDragon/badnest/issues/25
2019-11-03 17:28:27 -06:00
Jacob McSwain
c7ac77ca85
Merge pull request from USA-RedDragon/updates
Update readme and the gitignore
2019-11-03 17:14:00 -06:00
Jacob McSwain
dadf80f745
Update readme and the gitignore 2019-11-03 17:13:15 -06:00
Jacob McSwain
fa1a632d2b
Merge pull request from USA-RedDragon/multi-region-cameras
Add region config and use it to use correct camera URL
2019-11-03 17:08:23 -06:00
Vinod Mishra
b1447a4e6e
Add region config and use it to use correct camera URL 2019-11-03 17:05:05 -06:00
Jacob McSwain
3d49b85c50
Merge pull request from USA-RedDragon/cameras-multiple-api-instances
Attempt to make the camera's use different API instances
2019-10-23 21:48:30 -05:00
Jacob McSwain
4a6c88a6ef
Merge pull request from USA-RedDragon/api-fix-token-expiration
API: Handle KeyError to fix the expiration of tokens
2019-10-23 21:47:20 -05:00
Jacob McSwain
51efa759e8
Attempt to make the camera's use different API instances 2019-10-23 21:46:55 -05:00
Jacob McSwain
77bd36b4bd
API: Handle KeyError to fix the expiration of tokens 2019-10-23 21:29:24 -05:00
Jacob McSwain
1228304609
Merge pull request from USA-RedDragon/eco-visual-bug
Fix eco going in and out on UI
2019-10-22 13:47:22 -05:00
Jacob McSwain
68e71850be
Fix eco going in and out on UI 2019-10-22 13:46:47 -05:00
Jacob McSwain
5f4b14514c
Merge pull request from USA-RedDragon/start-error-handling
Add preliminary error handling support
2019-10-22 13:34:59 -05:00
Jacob McSwain
6f097c58a8
Merge pull request from USA-RedDragon/auto-eco-mode
Add auto eco mode to set of eco modes
2019-10-22 13:34:25 -05:00
Jacob McSwain
98daabc5d7
Add preliminary error handling support 2019-10-22 13:34:00 -05:00
Jacob McSwain
c4f702b859
Add auto eco mode to set of eco modes 2019-10-22 13:32:52 -05:00
9 changed files with 755 additions and 482 deletions

8
.gitignore vendored

@ -91,3 +91,11 @@ ENV/
.vscode/
.DS_Store
__init__.py
api.py
camera.py
climate.py
const.py
manifest.json
sensor.py

@ -1,21 +1,22 @@
"""The example integration."""
import voluptuous as vol
from homeassistant.helpers import config_validation as cv
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from .const import DOMAIN, CONF_ISSUE_TOKEN, CONF_COOKIE, CONF_APIKEY
from .api import NestAPI
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,
},
{
vol.Required(CONF_ISSUE_TOKEN, default=""): cv.string,
vol.Required(CONF_COOKIE, default=""): cv.string,
vol.Required(CONF_APIKEY, default=""): cv.string
vol.Optional(CONF_REGION, default="us"): cv.string,
}
)
},
@ -26,24 +27,26 @@ 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)
api_key = config[DOMAIN].get(CONF_APIKEY)
region = config[DOMAIN].get(CONF_REGION)
else:
email = None
password = None
issue_token = None
cookie = None
api_key = None
region = None
hass.data[DOMAIN] = {
CONF_EMAIL: email,
CONF_PASSWORD: password,
CONF_ISSUE_TOKEN: issue_token,
CONF_COOKIE: cookie,
CONF_APIKEY: api_key
'api': NestAPI(
user_id,
access_token,
issue_token,
cookie,
region,
),
}
return True

@ -1,57 +1,90 @@
import requests
import logging
import requests
API_URL = "https://home.nest.com"
CAMERA_WEBAPI_BASE = "https://webapi.camera.home.nest.com"
CAMERA_URL = "https://nexusapi-us1.camera.home.nest.com"
USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) " \
"AppleWebKit/537.36 (KHTML, like Gecko) " \
"Chrome/75.0.3770.100 Safari/537.36"
URL_JWT = "https://nestauthproxyservice-pa.googleapis.com/v1/issue_jwt"
# Nest website's (public) API key
NEST_API_KEY = "AIzaSyAdkSIMNc51XGNEAYWasX9UOWkS5P6sZE4"
KNOWN_BUCKET_TYPES = [
# Thermostats
"device",
"shared",
# Protect
"topaz",
# Temperature sensors
"kryptonite",
]
_LOGGER = logging.getLogger(__name__)
class NestAPI:
class NestAPI():
def __init__(self,
email,
password,
user_id,
access_token,
issue_token,
cookie,
api_key,
device_id=None):
self._user_id = None
self._access_token = None
region):
self.device_data = {}
self._wheres = {}
self._user_id = user_id
self._access_token = access_token
self._session = requests.Session()
self._session.headers.update({"Referer": "https://home.nest.com/"})
self._device_id = device_id
if not email and not password:
self._login_google(issue_token, cookie, api_key)
else:
self._login_nest(email, password)
self._session.headers.update({
"Referer": "https://home.nest.com/",
"User-Agent": USER_AGENT,
})
self._issue_token = issue_token
self._cookie = cookie
self._czfe_url = None
self._camera_url = f'https://nexusapi-{region}1.camera.home.nest.com'
self.cameras = []
self.thermostats = []
self.temperature_sensors = []
self.protects = []
self.login()
self._get_devices()
self.update()
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 __getitem__(self, name):
return getattr(self, name)
def _login_google(self, issue_token, cookie, api_key):
def __setitem__(self, name, value):
return setattr(self, name, value)
def __delitem__(self, name):
return delattr(self, name)
def __contains__(self, name):
return hasattr(self, name)
def login(self):
if self._issue_token and self._cookie:
self._login_google(self._issue_token, self._cookie)
self._login_dropcam()
def _login_google(self, issue_token, cookie):
headers = {
'Sec-Fetch-Mode': 'cors',
'User-Agent': USER_AGENT,
'Sec-Fetch-Mode': 'cors',
'X-Requested-With': 'XmlHttpRequest',
'Referer': 'https://accounts.google.com/o/oauth2/iframe',
'cookie': cookie
}
r = requests.get(url=issue_token, headers=headers)
r = self._session.get(url=issue_token, headers=headers)
access_token = r.json()['access_token']
headers = {
'Authorization': 'Bearer ' + access_token,
'User-Agent': USER_AGENT,
'x-goog-api-key': api_key,
'Authorization': 'Bearer ' + access_token,
'x-goog-api-key': NEST_API_KEY,
'Referer': 'https://home.nest.com'
}
params = {
@ -60,322 +93,446 @@ class NestAPI:
"google_oauth_access_token": access_token,
"policy_id": 'authproxy-oauth-policy'
}
r = requests.post(url=URL_JWT, headers=headers, params=params)
r = self._session.post(url=URL_JWT, headers=headers, params=params)
self._user_id = r.json()['claims']['subject']['nestId']['id']
self._access_token = r.json()['jwt']
class NestThermostatAPI(NestAPI):
def __init__(self,
email,
password,
issue_token,
cookie,
api_key,
device_id=None):
super(NestThermostatAPI, self).__init__(
email,
password,
issue_token,
cookie,
api_key,
device_id)
self._czfe_url = None
self._compressor_lockout_enabled = None
self._compressor_lockout_time = None
self._hvac_ac_state = None
self._hvac_heater_state = None
self.mode = None
self._time_to_target = None
self._fan_timer_timeout = None
self.can_heat = None
self.can_cool = None
self.has_fan = None
self.fan = None
self.current_temperature = None
self.target_temperature = None
self.target_temperature_high = None
self.target_temperature_low = None
self.current_humidity = None
self.update()
def get_devices(self):
r = self._session.post(
f"{API_URL}/api/0.1/user/{self._user_id}/app_launch",
json={
"known_bucket_types": ["buckets"],
"known_bucket_versions": [],
},
headers={"Authorization": f"Basic {self._access_token}"},
)
devices = []
buckets = r.json()['updated_buckets'][0]['value']['buckets']
for bucket in buckets:
if bucket.startswith('device.'):
devices.append(bucket.replace('device.', ''))
return devices
def get_action(self):
if self._hvac_ac_state:
return "cooling"
elif self._hvac_heater_state:
return "heating"
else:
return "off"
def update(self):
r = self._session.post(
f"{API_URL}/api/0.1/user/{self._user_id}/app_launch",
json={
"known_bucket_types": ["shared", "device"],
"known_bucket_versions": [],
},
headers={"Authorization": f"Basic {self._access_token}"},
)
self._czfe_url = r.json()["service_urls"]["urls"]["czfe_url"]
temp_mode = None
for bucket in r.json()["updated_buckets"]:
if bucket["object_key"].startswith(f"shared.{self._device_id}"):
thermostat_data = bucket["value"]
self.current_temperature = \
thermostat_data["current_temperature"]
self.target_temperature = thermostat_data["target_temperature"]
self._compressor_lockout_enabled = thermostat_data[
"compressor_lockout_enabled"
]
self._compressor_lockout_time = thermostat_data[
"compressor_lockout_timeout"
]
self._hvac_ac_state = thermostat_data["hvac_ac_state"]
self._hvac_heater_state = thermostat_data["hvac_heater_state"]
temp_mode = thermostat_data["target_temperature_type"]
self.target_temperature_high = thermostat_data[
"target_temperature_high"
]
self.target_temperature_low = \
thermostat_data["target_temperature_low"]
self.can_heat = thermostat_data["can_heat"]
self.can_cool = thermostat_data["can_cool"]
elif bucket["object_key"].startswith(f"device.{self._device_id}"):
thermostat_data = bucket["value"]
self._time_to_target = thermostat_data["time_to_target"]
self._fan_timer_timeout = thermostat_data["fan_timer_timeout"]
self.has_fan = thermostat_data["has_fan"]
self.fan = thermostat_data["fan_timer_timeout"] > 0
self.current_humidity = thermostat_data["current_humidity"]
if thermostat_data["eco"]["mode"] == 'manual-eco':
temp_mode = 'eco'
self.mode = temp_mode
def set_temp(self, temp, temp_high=None):
if temp_high is None:
self._session.post(
f"{self._czfe_url}/v5/put",
json={
"objects": [
{
"object_key": f'shared.{self._device_id}',
"op": "MERGE",
"value": {"target_temperature": temp},
}
]
},
headers={"Authorization": f"Basic {self._access_token}"},
)
else:
self._session.post(
f"{self._czfe_url}/v5/put",
json={
"objects": [
{
"object_key": f'shared.{self._device_id}',
"op": "MERGE",
"value": {
"target_temperature_low": temp,
"target_temperature_high": temp_high,
},
}
]
},
headers={"Authorization": f"Basic {self._access_token}"},
)
def set_mode(self, mode):
self._session.post(
f"{self._czfe_url}/v5/put",
json={
"objects": [
{
"object_key": f'shared.{self._device_id}',
"op": "MERGE",
"value": {"target_temperature_type": mode},
}
]
},
headers={"Authorization": f"Basic {self._access_token}"},
)
def set_fan(self, date):
self._session.post(
f"{self._czfe_url}/v5/put",
json={
"objects": [
{
"object_key": f'device.{self._device_id}',
"op": "MERGE",
"value": {"fan_timer_timeout": date},
}
]
},
headers={"Authorization": f"Basic {self._access_token}"},
)
def set_eco_mode(self, state):
mode = 'manual-eco' if state else 'schedule'
self._session.post(
f"{self._czfe_url}/v5/put",
json={
"objects": [
{
"object_key": f'device.{self._device_id}',
"op": "MERGE",
"value": {"eco": {"mode": mode}},
}
]
},
headers={"Authorization": f"Basic {self._access_token}"},
)
class NestTemperatureSensorAPI(NestAPI):
def __init__(self,
email,
password,
issue_token,
cookie,
api_key,
device_id=None):
super(NestTemperatureSensorAPI, self).__init__(
email,
password,
issue_token,
cookie,
api_key,
device_id)
self.temperature = None
self._device_id = device_id
self.update()
def get_devices(self):
r = self._session.post(
f"{API_URL}/api/0.1/user/{self._user_id}/app_launch",
json={
"known_bucket_types": ["buckets"],
"known_bucket_versions": [],
},
headers={"Authorization": f"Basic {self._access_token}"},
)
devices = []
buckets = r.json()['updated_buckets'][0]['value']['buckets']
for bucket in buckets:
if bucket.startswith('kryptonite.'):
devices.append(bucket.replace('kryptonite.', ''))
return devices
def update(self):
r = self._session.post(
f"{API_URL}/api/0.1/user/{self._user_id}/app_launch",
json={
"known_bucket_types": ["kryptonite"],
"known_bucket_versions": [],
},
headers={"Authorization": f"Basic {self._access_token}"},
)
for bucket in r.json()["updated_buckets"]:
if bucket["object_key"].startswith(
f"kryptonite.{self._device_id}"):
sensor_data = bucket["value"]
self.temperature = sensor_data["current_temperature"]
self.battery_level = sensor_data["battery_level"]
class NestCameraAPI(NestAPI):
def __init__(self, email, password, issue_token, cookie, api_key):
super(NestCameraAPI, self).__init__(
email,
password,
issue_token,
cookie,
api_key)
# log into dropcam
def _login_dropcam(self):
self._session.post(
f"{API_URL}/dropcam/api/login",
data={"access_token": self._access_token}
)
self.location = None
self.name = "Nest Camera"
self.online = None
self.is_streaming = None
self.battery_voltage = None
self.ac_voltge = None
self.data_tier = None
self.update()
def set_device(self, uuid):
self._device_id = uuid
self.update()
def _get_cameras(self):
cameras = []
try:
r = self._session.get(
f"{CAMERA_WEBAPI_BASE}/api/cameras."
+ "get_owned_and_member_of_with_properties"
)
for camera in r.json()["items"]:
cameras.append(camera['uuid'])
self.device_data[camera['uuid']] = {}
return cameras
except requests.exceptions.RequestException as e:
_LOGGER.error(e)
_LOGGER.error('Failed to get cameras, trying again')
return self._get_cameras()
except KeyError:
_LOGGER.debug('Failed to get cameras, trying to log in again')
self.login()
return self._get_cameras()
def _get_devices(self):
try:
r = self._session.post(
f"{API_URL}/api/0.1/user/{self._user_id}/app_launch",
json={
"known_bucket_types": ["buckets"],
"known_bucket_versions": [],
},
headers={"Authorization": f"Basic {self._access_token}"},
)
self._czfe_url = r.json()["service_urls"]["urls"]["czfe_url"]
buckets = r.json()['updated_buckets'][0]['value']['buckets']
for bucket in buckets:
if bucket.startswith('topaz.'):
sn = bucket.replace('topaz.', '')
self.protects.append(sn)
self.device_data[sn] = {}
elif bucket.startswith('kryptonite.'):
sn = bucket.replace('kryptonite.', '')
self.temperature_sensors.append(sn)
self.device_data[sn] = {}
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()
except requests.exceptions.RequestException as e:
_LOGGER.error(e)
_LOGGER.error('Failed to get devices, trying again')
return self.get_devices()
except KeyError:
_LOGGER.debug('Failed to get devices, trying to log in again')
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):
if self._device_id:
props = self.get_properties()
self._location = None
self.name = props["name"]
self.online = props["is_online"]
self.is_streaming = props["is_streaming"]
self.battery_voltage = props["rq_battery_battery_volt"]
self.ac_voltge = props["rq_battery_vbridge_volt"]
self.location = props["location"]
self.data_tier = props["properties"]["streaming.data-usage-tier"]
try:
# To get friendly names
r = self._session.post(
f"{API_URL}/api/0.1/user/{self._user_id}/app_launch",
json={
"known_bucket_types": ["where"],
"known_bucket_versions": [],
},
headers={"Authorization": f"Basic {self._access_token}"},
)
def _set_properties(self, property, value):
r = self._session.post(
f"{CAMERA_WEBAPI_BASE}/api/dropcams.set_properties",
data={property: value, "uuid": self._device_id},
)
for bucket in r.json()["updated_buckets"]:
sensor_data = bucket["value"]
sn = bucket["object_key"].split('.')[1]
if bucket["object_key"].startswith(
f"where.{sn}"):
wheres = sensor_data['wheres']
for where in wheres:
self._wheres[where['where_id']] = where['name']
return r.json()["items"]
r = self._session.post(
f"{API_URL}/api/0.1/user/{self._user_id}/app_launch",
json={
"known_bucket_types": KNOWN_BUCKET_TYPES,
"known_bucket_versions": [],
},
headers={"Authorization": f"Basic {self._access_token}"},
)
def get_properties(self):
r = self._session.get(
f"{API_URL}/dropcam/api/cameras/{self._device_id}"
)
return r.json()[0]
for bucket in r.json()["updated_buckets"]:
sensor_data = bucket["value"]
sn = bucket["object_key"].split('.')[1]
# Thermostats (thermostat and sensors system)
if bucket["object_key"].startswith(
f"shared.{sn}"):
self.device_data[sn]['current_temperature'] = \
sensor_data["current_temperature"]
self.device_data[sn]['target_temperature'] = \
sensor_data["target_temperature"]
self.device_data[sn]['hvac_ac_state'] = \
sensor_data["hvac_ac_state"]
self.device_data[sn]['hvac_heater_state'] = \
sensor_data["hvac_heater_state"]
self.device_data[sn]['target_temperature_high'] = \
sensor_data["target_temperature_high"]
self.device_data[sn]['target_temperature_low'] = \
sensor_data["target_temperature_low"]
self.device_data[sn]['can_heat'] = \
sensor_data["can_heat"]
self.device_data[sn]['can_cool'] = \
sensor_data["can_cool"]
self.device_data[sn]['mode'] = \
sensor_data["target_temperature_type"]
if self.device_data[sn]['hvac_ac_state']:
self.device_data[sn]['action'] = "cooling"
elif self.device_data[sn]['hvac_heater_state']:
self.device_data[sn]['action'] = "heating"
else:
self.device_data[sn]['action'] = "off"
# Thermostats, pt 2
elif bucket["object_key"].startswith(
f"device.{sn}"):
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']
def get_cameras(self):
r = self._session.get(
f"{CAMERA_WEBAPI_BASE}/api/cameras."
+ "get_owned_and_member_of_with_properties"
)
return r.json()["items"]
if sensor_data.get('description', None):
self.device_data[sn]['name'] += \
f' ({sensor_data["description"]})'
self.device_data[sn]['name'] += ' Thermostat'
self.device_data[sn]['has_fan'] = \
sensor_data["has_fan"]
self.device_data[sn]['fan'] = \
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
else:
self.device_data[sn]['eco'] = False
# Protect
elif bucket["object_key"].startswith(
f"topaz.{sn}"):
self.device_data[sn]['name'] = self._wheres[
sensor_data['where_id']
]
if sensor_data.get('description', None):
self.device_data[sn]['name'] += \
f' ({sensor_data["description"]})'
self.device_data[sn]['name'] += ' Protect'
self.device_data[sn]['co_status'] = \
self._map_nest_protect_state(sensor_data['co_status'])
self.device_data[sn]['smoke_status'] = \
self._map_nest_protect_state(sensor_data['smoke_status'])
self.device_data[sn]['battery_health_state'] = \
self._map_nest_protect_state(sensor_data['battery_health_state'])
# Temperature sensors
elif bucket["object_key"].startswith(
f"kryptonite.{sn}"):
self.device_data[sn]['name'] = self._wheres[
sensor_data['where_id']
]
if sensor_data.get('description', None):
self.device_data[sn]['name'] += \
f' ({sensor_data["description"]})'
self.device_data[sn]['name'] += ' Temperature'
self.device_data[sn]['temperature'] = \
sensor_data['current_temperature']
self.device_data[sn]['battery_level'] = \
sensor_data['battery_level']
# def set_upload_quality(self, quality):
# quality = str(quality)
# return self._set_properties("streaming.data-usage-tier", quality)
# Cameras
for camera in self.cameras:
r = self._session.get(
f"{API_URL}/dropcam/api/cameras/{camera}"
)
sensor_data = r.json()[0]
self.device_data[camera]['name'] = \
sensor_data["name"]
self.device_data[camera]['is_online'] = \
sensor_data["is_online"]
self.device_data[camera]['is_streaming'] = \
sensor_data["is_streaming"]
self.device_data[camera]['battery_voltage'] = \
sensor_data["rq_battery_battery_volt"]
self.device_data[camera]['ac_voltage'] = \
sensor_data["rq_battery_vbridge_volt"]
self.device_data[camera]['location'] = \
sensor_data["location"]
self.device_data[camera]['data_tier'] = \
sensor_data["properties"]["streaming.data-usage-tier"]
except requests.exceptions.RequestException as e:
_LOGGER.error(e)
_LOGGER.error('Failed to update, trying again')
self.update()
except KeyError:
_LOGGER.debug('Failed to update, trying to log in again')
self.login()
self.update()
def turn_off(self):
return self._set_properties("streaming.enabled", "false")
def thermostat_set_temperature(self, device_id, temp, temp_high=None):
if device_id not in self.thermostats:
return
def turn_on(self):
return self._set_properties("streaming.enabled", "true")
try:
if temp_high is None:
self._session.post(
f"{self._czfe_url}/v5/put",
json={
"objects": [
{
"object_key": f'shared.{device_id}',
"op": "MERGE",
"value": {"target_temperature": temp},
}
]
},
headers={"Authorization": f"Basic {self._access_token}"},
)
else:
self._session.post(
f"{self._czfe_url}/v5/put",
json={
"objects": [
{
"object_key": f'shared.{device_id}',
"op": "MERGE",
"value": {
"target_temperature_low": temp,
"target_temperature_high": temp_high,
},
}
]
},
headers={"Authorization": f"Basic {self._access_token}"},
)
except requests.exceptions.RequestException as e:
_LOGGER.error(e)
_LOGGER.error('Failed to set temperature, trying again')
self.thermostat_set_temperature(device_id, temp, temp_high)
except KeyError:
_LOGGER.debug('Failed to set temperature, trying to log in again')
self.login()
self.thermostat_set_temperature(device_id, temp, temp_high)
def get_image(self, now):
r = self._session.get(
f"{CAMERA_URL}/get_image?uuid={self._device_id}&cachebuster={now}"
)
def thermostat_set_target_humidity(self, device_id, humidity):
if device_id not in self.thermostats:
return
return r.content
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
try:
self._session.post(
f"{self._czfe_url}/v5/put",
json={
"objects": [
{
"object_key": f'shared.{device_id}',
"op": "MERGE",
"value": {"target_temperature_type": mode},
}
]
},
headers={"Authorization": f"Basic {self._access_token}"},
)
except requests.exceptions.RequestException as e:
_LOGGER.error(e)
_LOGGER.error('Failed to set mode, trying again')
self.thermostat_set_mode(device_id, mode)
except KeyError:
_LOGGER.debug('Failed to set mode, trying to log in again')
self.login()
self.thermostat_set_mode(device_id, mode)
def thermostat_set_fan(self, device_id, date):
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": {"fan_timer_timeout": date},
}
]
},
headers={"Authorization": f"Basic {self._access_token}"},
)
except requests.exceptions.RequestException as e:
_LOGGER.error(e)
_LOGGER.error('Failed to set fan, trying again')
self.thermostat_set_fan(device_id, date)
except KeyError:
_LOGGER.debug('Failed to set fan, trying to log in again')
self.login()
self.thermostat_set_fan(device_id, date)
def thermostat_set_eco_mode(self, device_id, state):
if device_id not in self.thermostats:
return
try:
mode = 'manual-eco' if state else 'schedule'
self._session.post(
f"{self._czfe_url}/v5/put",
json={
"objects": [
{
"object_key": f'device.{device_id}',
"op": "MERGE",
"value": {"eco": {"mode": mode}},
}
]
},
headers={"Authorization": f"Basic {self._access_token}"},
)
except requests.exceptions.RequestException as e:
_LOGGER.error(e)
_LOGGER.error('Failed to set eco, trying again')
self.thermostat_set_eco_mode(device_id, state)
except KeyError:
_LOGGER.debug('Failed to set eco, trying to log in again')
self.login()
self.thermostat_set_eco_mode(device_id, state)
def _camera_set_properties(self, device_id, property, value):
if device_id not in self.cameras:
return
try:
r = self._session.post(
f"{CAMERA_WEBAPI_BASE}/api/dropcams.set_properties",
data={property: value, "uuid": device_id},
)
return r.json()["items"]
except requests.exceptions.RequestException as e:
_LOGGER.error(e)
_LOGGER.error('Failed to set camera property, trying again')
return self._camera_set_properties(device_id, property, value)
except KeyError:
_LOGGER.debug('Failed to set camera property, ' +
'trying to log in again')
self.login()
return self._camera_set_properties(device_id, property, value)
def camera_turn_off(self, device_id):
if device_id not in self.cameras:
return
return self._set_properties(device_id, "streaming.enabled", "false")
def camera_turn_on(self, device_id):
if device_id not in self.cameras:
return
return self._set_properties(device_id, "streaming.enabled", "true")
def camera_get_image(self, device_id, now):
if device_id not in self.cameras:
return
try:
r = self._session.get(
f'{self._camera_url}/get_image?uuid={device_id}' +
f'&cachebuster={now}'
)
return r.content
except requests.exceptions.RequestException as e:
_LOGGER.error(e)
_LOGGER.error('Failed to get camera image, trying again')
return self.camera_get_image(device_id, now)
except KeyError:
_LOGGER.debug('Failed to get camera image, trying to log in again')
self.login()
return self.camera_get_image(device_id, now)

@ -1,44 +1,32 @@
"""This component provides basic support for Foscam IP cameras."""
import logging
from datetime import timedelta
from homeassistant.components.camera import (
Camera,
SUPPORT_ON_OFF,
)
from homeassistant.util.dt import utcnow
from homeassistant.components.camera import Camera, SUPPORT_ON_OFF
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from .api import NestCameraAPI
from .const import DOMAIN, CONF_ISSUE_TOKEN, CONF_COOKIE, CONF_APIKEY
from .const import (
DOMAIN
)
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Nest Camera"
DATA_KEY = "camera.badnest"
async def async_setup_platform(hass,
config,
async_add_entities,
discovery_info=None):
"""Set up a Nest Camera."""
hass.data[DATA_KEY] = dict()
api = hass.data[DOMAIN]['api']
api = NestCameraAPI(
hass.data[DOMAIN][CONF_EMAIL],
hass.data[DOMAIN][CONF_PASSWORD],
hass.data[DOMAIN][CONF_ISSUE_TOKEN],
hass.data[DOMAIN][CONF_COOKIE],
hass.data[DOMAIN][CONF_APIKEY]
)
# cameras = await hass.async_add_executor_job(nest.get_cameras())
cameras = []
_LOGGER.info("Adding cameras")
for camera in api.get_cameras():
_LOGGER.info("Adding nest cam uuid: %s", camera["uuid"])
device = NestCamera(camera["uuid"], api)
cameras.append(device)
hass.data[DATA_KEY][camera["uuid"]] = device
_LOGGER.info("Adding temperature sensors")
for camera in api['cameras']:
_LOGGER.info(f"Adding nest camera uuid: {camera}")
cameras.append(NestCamera(camera, api))
async_add_entities(cameras)
@ -50,7 +38,6 @@ class NestCamera(Camera):
"""Initialize a Nest camera."""
super().__init__()
self._uuid = uuid
api.set_device(self._uuid)
self._device = api
self._time_between_snapshots = timedelta(seconds=30)
self._last_image = None
@ -61,7 +48,7 @@ class NestCamera(Camera):
"""Return information about the device."""
return {
"identifiers": {(DOMAIN, self._uuid)},
"name": self.name,
"name": self._device.device_data[self._uuid]['name'],
"manufacturer": "Nest Labs",
"model": "Camera",
}
@ -78,20 +65,20 @@ class NestCamera(Camera):
@property
def is_on(self):
"""Return true if on."""
return self._device.online
return self._device.device_data[self._uuid]['is_online']
@property
def is_recording(self):
return True
"""Return true if the device is recording."""
return self._device.is_streaming
return self._device.device_data[self._uuid]['is_streaming']
def turn_off(self):
self._device.turn_off()
self._device.camera_turn_off(self._uuid)
self.schedule_update_ha_state()
def turn_on(self):
self._device.turn_on()
self._device.camera_turn_on(self._uuid)
self.schedule_update_ha_state()
@property
@ -106,7 +93,7 @@ class NestCamera(Camera):
@property
def name(self):
"""Return the name of this camera."""
return self._device.name
return self._device.device_data[self._uuid]['name']
def _ready_for_snapshot(self, now):
return self._next_snapshot_at is None or now > self._next_snapshot_at
@ -115,8 +102,7 @@ class NestCamera(Camera):
"""Return a still image response from the camera."""
now = utcnow()
if self._ready_for_snapshot(now) or True:
image = self._device.get_image(now)
# _LOGGER.info(image)
image = self._device.camera_get_image(self._uuid, now)
self._next_snapshot_at = now + self._time_between_snapshots
self._last_image = image

@ -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,
@ -25,12 +26,11 @@ from homeassistant.components.climate.const import (
from homeassistant.const import (
ATTR_TEMPERATURE,
TEMP_CELSIUS,
CONF_EMAIL,
CONF_PASSWORD,
)
from .api import NestThermostatAPI
from .const import DOMAIN, CONF_ISSUE_TOKEN, CONF_COOKIE, CONF_APIKEY
from .const import (
DOMAIN,
)
NEST_MODE_HEAT_COOL = "range"
NEST_MODE_ECO = "eco"
@ -53,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__)
@ -63,26 +67,13 @@ async def async_setup_platform(hass,
async_add_entities,
discovery_info=None):
"""Set up the Nest climate device."""
api = NestThermostatAPI(
hass.data[DOMAIN][CONF_EMAIL],
hass.data[DOMAIN][CONF_PASSWORD],
hass.data[DOMAIN][CONF_ISSUE_TOKEN],
hass.data[DOMAIN][CONF_COOKIE],
hass.data[DOMAIN][CONF_APIKEY]
)
api = hass.data[DOMAIN]['api']
thermostats = []
_LOGGER.info("Adding thermostats")
for thermostat in api.get_devices():
for thermostat in api['thermostats']:
_LOGGER.info(f"Adding nest thermostat uuid: {thermostat}")
thermostats.append(NestClimate(thermostat, NestThermostatAPI(
hass.data[DOMAIN][CONF_EMAIL],
hass.data[DOMAIN][CONF_PASSWORD],
hass.data[DOMAIN][CONF_ISSUE_TOKEN],
hass.data[DOMAIN][CONF_COOKIE],
hass.data[DOMAIN][CONF_APIKEY],
thermostat
)))
thermostats.append(NestClimate(thermostat, api))
async_add_entities(thermostats)
@ -105,28 +96,38 @@ class NestClimate(ClimateDevice):
self.device = api
if self.device.can_heat and self.device.can_cool:
if self.device.device_data[device_id]['can_heat'] \
and self.device.device_data[device_id]['can_cool']:
self._operation_list.append(HVAC_MODE_AUTO)
self._support_flags |= SUPPORT_TARGET_TEMPERATURE_RANGE
# Add supported nest thermostat features
if self.device.can_heat:
if self.device.device_data[device_id]['can_heat']:
self._operation_list.append(HVAC_MODE_HEAT)
if self.device.can_cool:
if self.device.device_data[device_id]['can_cool']:
self._operation_list.append(HVAC_MODE_COOL)
self._operation_list.append(HVAC_MODE_OFF)
# feature of device
if self.device.has_fan:
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."""
return self.device_id
@property
def name(self):
"""Return an friendly name."""
return self.device.device_data[self.device_id]['name']
@property
def supported_features(self):
"""Return the list of supported features."""
@ -137,11 +138,6 @@ class NestClimate(ClimateDevice):
"""Return the polling state."""
return True
@property
def name(self):
"""Return the name of the climate device."""
return self.device_id
@property
def temperature_unit(self):
"""Return the unit of measurement."""
@ -150,53 +146,78 @@ class NestClimate(ClimateDevice):
@property
def current_temperature(self):
"""Return the current temperature."""
return self.device.current_temperature
return self.device.device_data[self.device_id]['current_temperature']
@property
def current_humidity(self):
"""Return the current humidity."""
return self.device.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."""
if self.device.mode not in (NEST_MODE_HEAT_COOL, NEST_MODE_ECO):
return self.device.target_temperature
if self.device.device_data[self.device_id]['mode'] \
!= NEST_MODE_HEAT_COOL \
and not self.device.device_data[self.device_id]['eco']:
return \
self.device.device_data[self.device_id]['target_temperature']
return None
@property
def target_temperature_high(self):
"""Return the highbound target temperature we try to reach."""
if self.device.mode == NEST_MODE_ECO:
# TODO: Grab properly
return None
if self.device.mode == NEST_MODE_HEAT_COOL:
return self.device.target_temperature_high
if self.device.device_data[self.device_id]['mode'] \
== NEST_MODE_HEAT_COOL \
and not self.device.device_data[self.device_id]['eco']:
return \
self.device. \
device_data[self.device_id]['target_temperature_high']
return None
@property
def target_temperature_low(self):
"""Return the lowbound target temperature we try to reach."""
if self.device.mode == NEST_MODE_ECO:
# TODO: Grab properly
return None
if self.device.mode == NEST_MODE_HEAT_COOL:
return self.device.target_temperature_low
if self.device.device_data[self.device_id]['mode'] \
== NEST_MODE_HEAT_COOL \
and not self.device.device_data[self.device_id]['eco']:
return \
self.device. \
device_data[self.device_id]['target_temperature_low']
return None
@property
def hvac_action(self):
"""Return current operation ie. heat, cool, idle."""
return ACTION_NEST_TO_HASS[self.device.get_action()]
return ACTION_NEST_TO_HASS[
self.device.device_data[self.device_id]['action']
]
@property
def hvac_mode(self):
"""Return hvac target hvac state."""
if self.device.mode is None or self.device.mode == NEST_MODE_ECO:
if self.device.device_data[self.device_id]['mode'] is None \
or self.device.device_data[self.device_id]['eco']:
# We assume the first operation in operation list is the main one
return self._operation_list[0]
return MODE_NEST_TO_HASS[self.device.mode]
return MODE_NEST_TO_HASS[
self.device.device_data[self.device_id]['mode']
]
@property
def hvac_modes(self):
@ -206,7 +227,7 @@ class NestClimate(ClimateDevice):
@property
def preset_mode(self):
"""Return current preset mode."""
if self.device.mode == NEST_MODE_ECO:
if self.device.device_data[self.device_id]['eco']:
return PRESET_ECO
return PRESET_NONE
@ -219,16 +240,19 @@ class NestClimate(ClimateDevice):
@property
def fan_mode(self):
"""Return whether the fan is on."""
if self.device.has_fan:
if self.device.device_data[self.device_id]['has_fan']:
# Return whether the fan is on
return FAN_ON if self.device.fan else FAN_AUTO
if self.device.device_data[self.device_id]['fan']:
return FAN_ON
else:
return FAN_AUTO
# No Fan available so disable slider
return None
@property
def fan_modes(self):
"""Return the list of available fan modes."""
if self.device.has_fan:
if self.device.device_data[self.device_id]['has_fan']:
return self._fan_modes
return None
@ -237,33 +261,64 @@ class NestClimate(ClimateDevice):
temp = None
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
if self.device.mode == NEST_MODE_HEAT_COOL:
if self.device.device_data[self.device_id]['mode'] == \
NEST_MODE_HEAT_COOL:
if target_temp_low is not None and target_temp_high is not None:
self.device.set_temp(target_temp_low, target_temp_high)
self.device.thermostat_set_temperature(
self.device_id,
target_temp_low,
target_temp_high,
)
else:
temp = kwargs.get(ATTR_TEMPERATURE)
if temp is not None:
self.device.set_temp(temp)
self.device.thermostat_set_temperature(
self.device_id,
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.set_mode(MODE_HASS_TO_NEST[hvac_mode])
self.device.thermostat_set_mode(
self.device_id,
MODE_HASS_TO_NEST[hvac_mode],
)
def set_fan_mode(self, fan_mode):
"""Turn fan on/off."""
if self.device.has_fan:
if self.device.device_data[self.device_id]['has_fan']:
if fan_mode == "on":
self.device.set_fan(int(datetime.now().timestamp() + 60 * 30))
self.device.thermostat_set_fan(
self.device_id,
int(datetime.now().timestamp() + 60 * 30),
)
else:
self.device.set_fan(0)
self.device.thermostat_set_fan(
self.device_id,
0,
)
def set_preset_mode(self, preset_mode):
"""Set preset mode."""
need_eco = preset_mode in (PRESET_ECO)
is_eco = self.device.mode == NEST_MODE_ECO
need_eco = preset_mode == PRESET_ECO
if is_eco != need_eco:
self.device.set_eco_mode(need_eco)
if need_eco != self.device.device_data[self.device_id]['eco']:
self.device.thermostat_set_eco_mode(
self.device_id,
need_eco,
)
def update(self):
"""Updates data"""

@ -1,4 +1,6 @@
DOMAIN = 'badnest'
CONF_ISSUE_TOKEN = 'issue_token'
CONF_COOKIE = 'cookie'
CONF_APIKEY = 'api_key'
CONF_USER_ID = 'user_id'
CONF_ACCESS_TOKEN = 'access_token'
CONF_REGION = 'region'

@ -2,54 +2,50 @@ import logging
from homeassistant.helpers.entity import Entity
from .api import NestTemperatureSensorAPI
from .const import DOMAIN, CONF_APIKEY, CONF_COOKIE, CONF_ISSUE_TOKEN
from .const import DOMAIN
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
DEVICE_CLASS_TEMPERATURE,
CONF_EMAIL,
CONF_PASSWORD,
TEMP_CELSIUS
)
_LOGGER = logging.getLogger(__name__)
PROTECT_SENSOR_TYPES = [
"co_status",
"smoke_status",
"battery_health_state"
]
async def async_setup_platform(hass,
config,
async_add_entities,
discovery_info=None):
"""Set up the Nest climate device."""
api = NestTemperatureSensorAPI(
hass.data[DOMAIN][CONF_EMAIL],
hass.data[DOMAIN][CONF_PASSWORD],
hass.data[DOMAIN][CONF_ISSUE_TOKEN],
hass.data[DOMAIN][CONF_COOKIE],
hass.data[DOMAIN][CONF_APIKEY]
)
api = hass.data[DOMAIN]['api']
sensors = []
temperature_sensors = []
_LOGGER.info("Adding temperature sensors")
for sensor in api.get_devices():
for sensor in api['temperature_sensors']:
_LOGGER.info(f"Adding nest temp sensor uuid: {sensor}")
sensors.append(
NestTemperatureSensor(
sensor,
NestTemperatureSensorAPI(
hass.data[DOMAIN][CONF_EMAIL],
hass.data[DOMAIN][CONF_PASSWORD],
hass.data[DOMAIN][CONF_ISSUE_TOKEN],
hass.data[DOMAIN][CONF_COOKIE],
hass.data[DOMAIN][CONF_APIKEY],
sensor
)))
temperature_sensors.append(NestTemperatureSensor(sensor, api))
async_add_entities(sensors)
async_add_entities(temperature_sensors)
protect_sensors = []
_LOGGER.info("Adding protect sensors")
for sensor in api['protects']:
_LOGGER.info(f"Adding nest protect sensor uuid: {sensor}")
for sensor_type in PROTECT_SENSOR_TYPES:
protect_sensors.append(NestProtectSensor(sensor, sensor_type, api))
async_add_entities(protect_sensors)
class NestTemperatureSensor(Entity):
"""Implementation of the DHT sensor."""
"""Implementation of the Nest Temperature Sensor."""
def __init__(self, device_id, api):
"""Initialize the sensor."""
@ -58,15 +54,20 @@ class NestTemperatureSensor(Entity):
self.device_id = device_id
self.device = api
@property
def unique_id(self):
"""Return an unique ID."""
return self.device_id
@property
def name(self):
"""Return the name of the sensor."""
return self.device_id
return self.device.device_data[self.device_id]['name']
@property
def state(self):
"""Return the state of the sensor."""
return self.device.temperature
return self.device.device_data[self.device_id]['temperature']
@property
def device_class(self):
@ -85,4 +86,38 @@ class NestTemperatureSensor(Entity):
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {ATTR_BATTERY_LEVEL: self.device.battery_level}
return {
ATTR_BATTERY_LEVEL:
self.device.device_data[self.device_id]['battery_level']
}
class NestProtectSensor(Entity):
"""Implementation of the Nest Protect sensor."""
def __init__(self, device_id, sensor_type, api):
"""Initialize the sensor."""
self._name = "Nest Protect Sensor"
self.device_id = device_id
self._sensor_type = sensor_type
self.device = api
@property
def unique_id(self):
"""Return an unique ID."""
return self.device_id + '_' + self._sensor_type
@property
def name(self):
"""Return the name of the sensor."""
return self.device.device_data[self.device_id]['name'] + \
f' {self._sensor_type}'
@property
def state(self):
"""Return the state of the sensor."""
return self.device.device_data[self.device_id][self._sensor_type]
def update(self):
"""Get the latest data from the Protect and updates the states."""
self.device.update()

@ -1,4 +1,4 @@
{
"name": "Bad Nest Thermostat",
"name": "Bad Nest",
"domains": ["climate", "camera", "sensor"]
}

59
info.md

@ -1,26 +1,49 @@
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 good 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
## Features
- Doesn't use the now defunct Works with Nest API
- Works with migrated/new accounts via Google auth
- Nest Protect support
- Nest Thermostat support
- Nest Thermostat Sensor support
- Nest Camera support
## Drawbacks
- No proper error handling
- 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 could change their webapp api at any time, making this defunct
- Thermostat presets don't work (Eco, Away)
- Nest Protect integration is untested by me
- 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>)
## Example configuration.yaml - When you're not using the Google Auth Login
## Configuration
The camera's region is one of `us` or `eu` depending on your region.
If you're not in the US or EU, you should be able to add your
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:
- platform: badnest
@ -33,13 +56,13 @@ sensor:
- platform: badnest
```
## Example configuration.yaml - When you are using the Google Auth Login
### Example configuration.yaml - When you are using the Google Auth Login
```yaml
badnest:
issue_token: "https://accounts.google.com/o/oauth2/iframerpc....."
cookie: "OCAK=......"
api_key : "#YOURAPIKEYHERE#"
region: us
climate:
- platform: badnest
@ -54,7 +77,7 @@ sensor:
Google Login support added with many thanks to: chrisjshull from <https://github.com/chrisjshull/homebridge-nest/>
The values of `"issue_token"`, `"cookie"` and `"api_key"` 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).
The values of `"issue_token"` and `"cookie"` 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).
@ -66,6 +89,10 @@ The values of `"issue_token"`, `"cookie"` and `"api_key"` are specific to your G
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`.
11. In the 'Filter' box, enter `issue_jwt`
12. Click on the last `issue_jwt` call.
13. In the Headers tab, under Request Headers, copy the entire `x-goog-api-key` (do not include the `x-goog-api-key:` name). This is your `"api_key"` 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.