From 334ce8579f5ace7ea55505f04bef18844f6ae408 Mon Sep 17 00:00:00 2001 From: Jacob McSwain Date: Sun, 3 Nov 2019 21:43:45 -0600 Subject: [PATCH] Rewrite API to be a singleton This should make error handling and extending this to more Nest platforms much easier --- custom_components/badnest/__init__.py | 18 +- custom_components/badnest/api.py | 669 ++++++++++++++------------ custom_components/badnest/camera.py | 55 +-- custom_components/badnest/climate.py | 139 +++--- custom_components/badnest/sensor.py | 45 +- 5 files changed, 475 insertions(+), 451 deletions(-) diff --git a/custom_components/badnest/__init__.py b/custom_components/badnest/__init__.py index a4913f0..3abcbcf 100644 --- a/custom_components/badnest/__init__.py +++ b/custom_components/badnest/__init__.py @@ -3,7 +3,8 @@ 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 +from .api import NestAPI +from .const import DOMAIN, CONF_ISSUE_TOKEN, CONF_COOKIE, CONF_REGION CONFIG_SCHEMA = vol.Schema( { @@ -11,10 +12,12 @@ CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_EMAIL, default=""): cv.string, vol.Required(CONF_PASSWORD, 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.Optional(CONF_REGION, default="us"): cv.string, } ) }, @@ -29,17 +32,22 @@ def setup(hass, config): password = config[DOMAIN].get(CONF_PASSWORD) issue_token = config[DOMAIN].get(CONF_ISSUE_TOKEN) cookie = config[DOMAIN].get(CONF_COOKIE) + region = config[DOMAIN].get(CONF_REGION) else: email = None password = None issue_token = None cookie = None + region = None hass.data[DOMAIN] = { - CONF_EMAIL: email, - CONF_PASSWORD: password, - CONF_ISSUE_TOKEN: issue_token, - CONF_COOKIE: cookie, + 'api': NestAPI( + email, + password, + issue_token, + cookie, + region, + ), } return True diff --git a/custom_components/badnest/api.py b/custom_components/badnest/api.py index fe18ce2..ce8d339 100644 --- a/custom_components/badnest/api.py +++ b/custom_components/badnest/api.py @@ -1,10 +1,9 @@ -import requests import logging +import requests + API_URL = "https://home.nest.com" CAMERA_WEBAPI_BASE = "https://webapi.camera.home.nest.com" -CAMERA_US_URL = "https://nexusapi-us1.camera.home.nest.com" -CAMERA_EU_URL = "https://nexusapi-eu1.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" @@ -13,32 +12,64 @@ 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", + # Temperature sensors + "kryptonite", +] + _LOGGER = logging.getLogger(__name__) -class NestAPI: +class NestAPI(): def __init__(self, email, password, issue_token, cookie, - device_id=None): + region): + self.device_data = {} + self._wheres = {} self._user_id = None self._access_token = None self._session = requests.Session() - self._session.headers.update({"Referer": "https://home.nest.com/"}) - self._device_id = device_id + self._session.headers.update({ + "Referer": "https://home.nest.com/", + "User-Agent": USER_AGENT, + }) self._email = email self._password = password self._issue_token = issue_token self._cookie = cookie + self._czfe_url = None + self._camera_url = f'https://nexusapi-{region}1.camera.home.nest.com' + self.cameras = [] + self.thermostats = [] + self.temperature_sensors = [] self.login() + self._get_devices() + self.update() + + def __getitem__(self, name): + return getattr(self, name) + + 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 not self._email and not self._password: self._login_google(self._issue_token, self._cookie) else: self._login_nest(self._email, self._password) + self._login_dropcam() def _login_nest(self, email, password): r = self._session.post( @@ -49,18 +80,18 @@ class NestAPI: 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, + 'Authorization': 'Bearer ' + access_token, 'x-goog-api-key': NEST_API_KEY, 'Referer': 'https://home.nest.com' } @@ -70,44 +101,40 @@ 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'] + def _login_dropcam(self): + self._session.post( + f"{API_URL}/dropcam/api/login", + data={"access_token": self._access_token} + ) -class NestThermostatAPI(NestAPI): - def __init__(self, - email, - password, - issue_token, - cookie, - device_id=None): - super(NestThermostatAPI, self).__init__( - email, - password, - issue_token, - cookie, - 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_cameras(self): + cameras = [] - def get_devices(self): + 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", @@ -117,206 +144,22 @@ class NestThermostatAPI(NestAPI): }, 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 - 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 get_action(self): - if self._hvac_ac_state: - return "cooling" - elif self._hvac_heater_state: - return "heating" - else: - return "off" - - def update(self): - try: - 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"] - if temp_mode is None: - 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' or \ - thermostat_data["eco"]["mode"] == 'auto-eco': - temp_mode = 'eco' - self.mode = temp_mode - 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 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, - device_id=None): - super(NestTemperatureSensorAPI, self).__init__( - email, - password, - issue_token, - cookie, - device_id) - self.temperature = None - self._device_id = device_id - self.update() - - 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}"}, - ) - devices = [] buckets = r.json()['updated_buckets'][0]['value']['buckets'] for bucket in buckets: if bucket.startswith('kryptonite.'): - devices.append(bucket.replace('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.device_data[sn] = {} + + self.cameras = self._get_cameras() - return devices except requests.exceptions.RequestException as e: _LOGGER.error(e) _LOGGER.error('Failed to get devices, trying again') @@ -328,21 +171,120 @@ class NestTemperatureSensorAPI(NestAPI): def update(self): 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": ["kryptonite"], + "known_bucket_types": ["where"], "known_bucket_versions": [], }, headers={"Authorization": f"Basic {self._access_token}"}, ) for bucket in r.json()["updated_buckets"]: + sensor_data = bucket["value"] + sn = bucket["object_key"].split('.')[1] 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"] + f"where.{sn}"): + wheres = sensor_data['wheres'] + for where in wheres: + self._wheres[where['where_id']] = where['name'] + + 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}"}, + ) + + for bucket in r.json()["updated_buckets"]: + sensor_data = bucket["value"] + sn = bucket["object_key"].split('.')[1] + # Thermostats + 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'] + ] + 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"] + 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 + # 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'] + + # 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') @@ -352,91 +294,182 @@ class NestTemperatureSensorAPI(NestAPI): self.login() self.update() + def thermostat_set_temperature(self, device_id, temp, temp_high=None): + if device_id not in self.thermostats: + return -class NestCameraAPI(NestAPI): - def __init__(self, - email, - password, - issue_token, - cookie, - region, - device_id=None): - super(NestCameraAPI, self).__init__( - email, - password, - issue_token, - cookie, - device_id) - # log into dropcam - self._session.post( - f"{API_URL}/dropcam/api/login", - data={"access_token": self._access_token} - ) - self._device_id = device_id - 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 - if region.lower() == 'eu': - self._camera_url = CAMERA_EU_URL - elif region.lower() == 'us': - self._camera_url = CAMERA_US_URL - else: - lower_region = region.lower() - self._camera_url = \ - f'https://nexusapi-{lower_region}1.camera.home.nest.com' - self.update() + 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 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"] + def thermostat_set_mode(self, device_id, mode): + if device_id not in self.thermostats: + return - 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}, - ) + 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) - return r.json()["items"] + def thermostat_set_fan(self, device_id, date): + if device_id not in self.thermostats: + return - def get_properties(self): - r = self._session.get( - f"{API_URL}/dropcam/api/cameras/{self._device_id}" - ) - return r.json()[0] + 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 get_cameras(self): - r = self._session.get( - f"{CAMERA_WEBAPI_BASE}/api/cameras." - + "get_owned_and_member_of_with_properties" - ) - return r.json()["items"] + def thermostat_set_eco_mode(self, device_id, state): + if device_id not in self.thermostats: + return - # def set_upload_quality(self, quality): - # quality = str(quality) - # return self._set_properties("streaming.data-usage-tier", quality) + 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 turn_off(self): - return self._set_properties("streaming.enabled", "false") + def _camera_set_properties(self, device_id, property, value): + if device_id not in self.cameras: + return - def turn_on(self): - return self._set_properties("streaming.enabled", "true") + try: + r = self._session.post( + f"{CAMERA_WEBAPI_BASE}/api/dropcams.set_properties", + data={property: value, "uuid": device_id}, + ) - def get_image(self, now): - r = self._session.get( - f'{self._camera_url}/get_image?uuid={self._device_id}' + - f'&cachebuster={now}' - ) + 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) - return r.content + 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) diff --git a/custom_components/badnest/camera.py b/custom_components/badnest/camera.py index b348aac..c0e71de 100644 --- a/custom_components/badnest/camera.py +++ b/custom_components/badnest/camera.py @@ -2,61 +2,31 @@ import logging from datetime import timedelta -import voluptuous as vol - from homeassistant.components.camera import ( Camera, SUPPORT_ON_OFF, - PLATFORM_SCHEMA ) -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow -from .api import NestCameraAPI from .const import ( - CONF_COOKIE, - CONF_ISSUE_TOKEN, - CONF_REGION, DOMAIN ) _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_REGION, default="us"): cv.string, - } -) - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up a Nest Camera.""" - api = NestCameraAPI( - hass.data[DOMAIN][CONF_EMAIL], - hass.data[DOMAIN][CONF_PASSWORD], - hass.data[DOMAIN][CONF_ISSUE_TOKEN], - hass.data[DOMAIN][CONF_COOKIE], - config.get(CONF_REGION) - ) + api = hass.data[DOMAIN]['api'] - # 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"], NestCameraAPI( - hass.data[DOMAIN][CONF_EMAIL], - hass.data[DOMAIN][CONF_PASSWORD], - hass.data[DOMAIN][CONF_ISSUE_TOKEN], - hass.data[DOMAIN][CONF_COOKIE], - config.get(CONF_REGION), - camera["uuid"] - )) - cameras.append(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) @@ -78,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", } @@ -95,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]['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 @@ -123,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 @@ -132,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 diff --git a/custom_components/badnest/climate.py b/custom_components/badnest/climate.py index b1a79a9..78a2dbe 100644 --- a/custom_components/badnest/climate.py +++ b/custom_components/badnest/climate.py @@ -25,12 +25,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 +from .const import ( + DOMAIN, +) NEST_MODE_HEAT_COOL = "range" NEST_MODE_ECO = "eco" @@ -63,24 +62,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], - ) + 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], - thermostat - ))) + thermostats.append(NestClimate(thermostat, api)) async_add_entities(thermostats) @@ -103,21 +91,22 @@ 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 @property @@ -125,6 +114,11 @@ class NestClimate(ClimateDevice): """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.""" @@ -135,11 +129,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.""" @@ -148,53 +137,63 @@ 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_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): @@ -204,7 +203,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 @@ -217,16 +216,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 @@ -235,33 +237,52 @@ 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_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""" diff --git a/custom_components/badnest/sensor.py b/custom_components/badnest/sensor.py index cb2571d..31faba1 100644 --- a/custom_components/badnest/sensor.py +++ b/custom_components/badnest/sensor.py @@ -2,14 +2,11 @@ import logging from homeassistant.helpers.entity import Entity -from .api import NestTemperatureSensorAPI -from .const import DOMAIN, 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 ) @@ -21,33 +18,21 @@ async def async_setup_platform(hass, 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], - ) + 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], - sensor - ))) + temperature_sensors.append(NestTemperatureSensor(sensor, api)) + + async_add_entities(temperature_sensors) async_add_entities(sensors) class NestTemperatureSensor(Entity): - """Implementation of the DHT sensor.""" + """Implementation of the Nest Temperature Sensor.""" def __init__(self, device_id, api): """Initialize the sensor.""" @@ -56,15 +41,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): @@ -83,4 +73,7 @@ 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'] + }