mirror of
https://github.com/USA-RedDragon/badnest.git
synced 2025-04-30 09:34:33 +01:00
Compare commits
41 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
faa0c956db | ||
|
7ab48d968e | ||
|
59bb5ae237 | ||
|
d74dc3f2ea | ||
|
a5b57f0cf0 | ||
|
1069805ed8 | ||
|
857463f3d4 | ||
|
cbc8702ee0 | ||
|
f1ebd8a2f7 | ||
|
2e9378de06 | ||
|
5b9cf52aff | ||
|
5b3c70e0ec | ||
|
63da0e61b0 | ||
|
e55159b465 | ||
|
933b7a017b | ||
|
de8dfa1490 | ||
|
222f981692 | ||
|
cc7ec56d2a | ||
|
385e97a62a | ||
|
fe6f04e645 | ||
|
799d7a55dc | ||
|
0c87f7561a | ||
|
c9693df3a2 | ||
|
2927881c33 | ||
|
334ce8579f | ||
|
98490b08f5 | ||
|
7cef7c65a4 | ||
|
c7ac77ca85 | ||
|
dadf80f745 | ||
|
fa1a632d2b | ||
|
b1447a4e6e | ||
|
3d49b85c50 | ||
|
4a6c88a6ef | ||
|
51efa759e8 | ||
|
77bd36b4bd | ||
|
1228304609 | ||
|
68e71850be | ||
|
5f4b14514c | ||
|
6f097c58a8 | ||
|
98daabc5d7 | ||
|
c4f702b859 |
8
.gitignore
vendored
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
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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user