diff --git a/custom_components/badnest/__init__.py b/custom_components/badnest/__init__.py index 8d1a3ab..62c0902 100644 --- a/custom_components/badnest/__init__.py +++ b/custom_components/badnest/__init__.py @@ -1,11 +1,9 @@ """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 -from homeassistant.const import ( - CONF_EMAIL, - CONF_PASSWORD -) CONFIG_SCHEMA = vol.Schema( { @@ -18,23 +16,3 @@ CONFIG_SCHEMA = vol.Schema( }, extra=vol.ALLOW_EXTRA, ) - - -def setup(hass, config): - """Set up the asuswrt component.""" - if config.get(DOMAIN) is not None: - email = config[DOMAIN].get(CONF_EMAIL) - password = config[DOMAIN].get(CONF_PASSWORD) - else: - email = None - password = None - - from .api import NestAPI - api = NestAPI( - email, - password - ) - - hass.data[DOMAIN] = api - - return True diff --git a/custom_components/badnest/api.py b/custom_components/badnest/api.py index deb9251..cbc5005 100644 --- a/custom_components/badnest/api.py +++ b/custom_components/badnest/api.py @@ -1,13 +1,34 @@ import requests -API_URL = 'https://home.nest.com' +API_URL = "https://home.nest.com" +CAMERA_WEBAPI_BASE = "https://webapi.camera.home.nest.com" +CAMERA_URL = "https://nexusapi-us1.camera.home.nest.com" -class NestAPI(): +class NestAPI: def __init__(self, email, password): self._user_id = None self._access_token = None + self._session = requests.Session() + self._session.headers.update({"Referer": "https://home.nest.com/"}) self._device_id = None + self._login(email, password) + self.update() + + def _login(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 update(self): + raise NotImplementedError() + + +class NestThermostatAPI(NestAPI): + def __init__(self, email, password): + super(NestThermostatAPI, self).__init__(email, password) self._shared_id = None self._czfe_url = None self._compressor_lockout_enabled = None @@ -28,142 +49,204 @@ class NestAPI(): self.target_temperature_low = None self.current_humidity = None - self._login(email, password) - self.update() - - def _login(self, email, password): - r = requests.post(f'{API_URL}/session', json={ - 'email': email, - 'password': password - }) - self._user_id = r.json()['userid'] - self._access_token = r.json()['access_token'] - def get_action(self): if self._hvac_ac_state: - return 'cooling' + return "cooling" elif self._hvac_heater_state: - return 'heating' + return "heating" else: - return 'off' + return "off" def update(self): - r = requests.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}' - }) + 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'] + self._czfe_url = r.json()["service_urls"]["urls"]["czfe_url"] - for bucket in r.json()['updated_buckets']: - if bucket['object_key'].startswith('shared.'): - self._shared_id = bucket['object_key'] - thermostat_data = bucket['value'] + for bucket in r.json()["updated_buckets"]: + if bucket["object_key"].startswith("shared."): + self._shared_id = bucket["object_key"] + 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'] - self.mode = thermostat_data['target_temperature_type'] - self.target_temperature_high = \ - thermostat_data['target_temperature_high'] + 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"] + self.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('device.'): - self._device_id = bucket['object_key'] - 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'] - self.away = thermostat_data['home_away_input'] + thermostat_data["target_temperature_low"] + self.can_heat = thermostat_data["can_heat"] + self.can_cool = thermostat_data["can_cool"] + elif bucket["object_key"].startswith("device."): + self._device_id = bucket["object_key"] + 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"] + self.away = thermostat_data["home_away_input"] def set_temp(self, temp, temp_high=None): if temp_high is None: - requests.post(f'{self._czfe_url}/v5/put', - json={ - 'objects': [{ - 'object_key': self._shared_id, - 'op': 'MERGE', - 'value': { - 'target_temperature': temp - } - }] - }, - headers={ - 'Authorization': f'Basic {self._access_token}' - }) + self._session.post( + f"{self._czfe_url}/v5/put", + json={ + "objects": [ + { + "object_key": self._shared_id, + "op": "MERGE", + "value": {"target_temperature": temp}, + } + ] + }, + headers={"Authorization": f"Basic {self._access_token}"}, + ) else: - requests.post(f'{self._czfe_url}/v5/put', - json={ - 'objects': [{ - 'object_key': self._shared_id, - 'op': 'MERGE', - 'value': { - 'target_temperature_low': temp, - 'target_temperature_high': temp_high - } - }] - }, - headers={ - 'Authorization': f'Basic {self._access_token}' - }) + self._session.post( + f"{self._czfe_url}/v5/put", + json={ + "objects": [ + { + "object_key": self._shared_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): - requests.post(f'{self._czfe_url}/v5/put', - json={ - 'objects': [{ - 'object_key': self._shared_id, - 'op': 'MERGE', - 'value': { - 'target_temperature_type': mode - } - }] - }, - headers={ - 'Authorization': f'Basic {self._access_token}' - }) + self._session.post( + f"{self._czfe_url}/v5/put", + json={ + "objects": [ + { + "object_key": self._shared_id, + "op": "MERGE", + "value": {"target_temperature_type": mode}, + } + ] + }, + headers={"Authorization": f"Basic {self._access_token}"}, + ) def set_fan(self, date): - requests.post(f'{self._czfe_url}/v5/put', - json={ - 'objects': [{ - 'object_key': self._device_id, - 'op': 'MERGE', - 'value': { - 'fan_timer_timeout': date - } - }] - }, - headers={ - 'Authorization': f'Basic {self._access_token}' - }) + self._session.post( + f"{self._czfe_url}/v5/put", + json={ + "objects": [ + { + "object_key": self._device_id, + "op": "MERGE", + "value": {"fan_timer_timeout": date}, + } + ] + }, + headers={"Authorization": f"Basic {self._access_token}"}, + ) def set_eco_mode(self): - requests.post(f'{self._czfe_url}/v5/put', - json={ - 'objects': [{ - 'object_key': self._device_id, - 'op': 'MERGE', - 'value': { - 'eco': { - 'mode': 'manual-eco' - } - } - }] - }, - headers={ - 'Authorization': f'Basic {self._access_token}' - }) + self._session.post( + f"{self._czfe_url}/v5/put", + json={ + "objects": [ + { + "object_key": self._device_id, + "op": "MERGE", + "value": {"eco": {"mode": "manual-eco"}}, + } + ] + }, + headers={"Authorization": f"Basic {self._access_token}"}, + ) + + +class NestCameraAPI(NestAPI): + def __init__(self, email, password): + super(NestCameraAPI, self).__init__(email, password) + # log into dropcam + 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 + + def set_device(self, uuid): + self._device_id = uuid + self.update() + + 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 _set_properties(self, property, value): + r = self._session.post( + f"{CAMERA_WEBAPI_BASE}/api/dropcams.set_properties", + data={property: value, "uuid": self._device_id}, + ) + + return r.json()["items"] + + def get_properties(self): + r = self._session.get( + f"{API_URL}/dropcam/api/cameras/{self._device_id}" + ) + return r.json()[0] + + 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 set_upload_quality(self, quality): + # quality = str(quality) + # return self._set_properties("streaming.data-usage-tier", quality) + + def turn_off(self): + return self._set_properties("streaming.enabled", "false") + + def turn_on(self): + return self._set_properties("streaming.enabled", "true") + + def get_image(self, now): + r = self._session.get( + f"{CAMERA_URL}/get_image?uuid={self._device_id}&cachebuster={now}" + ) + + return r.content diff --git a/custom_components/badnest/camera.py b/custom_components/badnest/camera.py new file mode 100644 index 0000000..fb72232 --- /dev/null +++ b/custom_components/badnest/camera.py @@ -0,0 +1,119 @@ +"""This component provides basic support for Foscam IP cameras.""" +import logging +from datetime import timedelta +from homeassistant.util.dt import utcnow + +from homeassistant.components.camera import Camera, SUPPORT_ON_OFF + +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +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 Foscam IP Camera.""" + from .api import NestCameraAPI + + hass.data[DATA_KEY] = dict() + + api = NestCameraAPI(config.get(CONF_EMAIL), config.get(CONF_PASSWORD)) + + # 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 + + async_add_entities(cameras) + + +class NestCamera(Camera): + """An implementation of a Nest camera.""" + + def __init__(self, uuid, api): + """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 + self._next_snapshot_at = None + + @property + def device_info(self): + """Return information about the device.""" + return { + "identifiers": {(DOMAIN, self._uuid)}, + "name": self.name, + "manufacturer": "Nest Labs", + "model": "Camera", + } + + @property + def should_poll(self): + return True + + @property + def unique_id(self): + """Return an unique ID.""" + return self._uuid + + @property + def is_on(self): + """Return true if on.""" + return self._device.online + + @property + def is_recording(self): + return True + """Return true if the device is recording.""" + return self._device.is_streaming + + def turn_off(self): + self._device.turn_off() + self.schedule_update_ha_state() + + def turn_on(self): + self._device.turn_on() + self.schedule_update_ha_state() + + @property + def supported_features(self): + """Return supported features.""" + return SUPPORT_ON_OFF + + def update(self): + """Cache value from Python-nest.""" + self._device.update() + + @property + def name(self): + """Return the name of this camera.""" + return self._device.name + + def _ready_for_snapshot(self, now): + return self._next_snapshot_at is None or now > self._next_snapshot_at + + def camera_image(self): + """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) + + self._next_snapshot_at = now + self._time_between_snapshots + self._last_image = image + + return self._last_image diff --git a/custom_components/badnest/climate.py b/custom_components/badnest/climate.py index ffac6b9..22f3961 100644 --- a/custom_components/badnest/climate.py +++ b/custom_components/badnest/climate.py @@ -21,9 +21,14 @@ from homeassistant.components.climate.const import ( CURRENT_HVAC_IDLE, CURRENT_HVAC_COOL, ) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_TEMPERATURE, + TEMP_CELSIUS, + CONF_EMAIL, + CONF_PASSWORD, +) -from .const import DOMAIN +from .api import NestThermostatAPI NEST_MODE_HEAT_COOL = "range" NEST_MODE_ECO = "eco" @@ -51,13 +56,14 @@ PRESET_AWAY_AND_ECO = "Away and Eco" PRESET_MODES = [PRESET_NONE, PRESET_AWAY, PRESET_ECO, PRESET_AWAY_AND_ECO] -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Nest climate device.""" - add_entities( - [ - ShittyNestClimate(hass.data[DOMAIN]), - ] - ) +async def async_setup_platform(hass, + config, + async_add_entities, + discovery_info=None): + """Set up a Foscam IP Camera.""" + nest = NestThermostatAPI(config.get(CONF_EMAIL), config.get(CONF_PASSWORD)) + + async_add_entities(ShittyNestClimate(nest)) class ShittyNestClimate(ClimateDevice): @@ -65,7 +71,7 @@ class ShittyNestClimate(ClimateDevice): def __init__(self, api): """Initialize the thermostat.""" - self._name = "Nest" + self._name = "Nest Thermostat" self._unit_of_measurement = TEMP_CELSIUS self._fan_modes = [FAN_ON, FAN_AUTO] @@ -221,7 +227,7 @@ class ShittyNestClimate(ClimateDevice): def set_fan_mode(self, fan_mode): """Turn fan on/off.""" if self.device.has_fan: - if fan_mode == 'on': + if fan_mode == "on": self.device.set_fan(int(datetime.now().timestamp() + 60 * 30)) else: self.device.set_fan(0) diff --git a/custom_components/badnest/services.yaml b/custom_components/badnest/services.yaml new file mode 100644 index 0000000..e69de29