1
0
mirror of https://github.com/USA-RedDragon/badnest.git synced 2025-01-18 18:30:43 +00:00

Inital commit

This commit is contained in:
Jacob McSwain 2019-09-25 19:14:43 -05:00
commit 262622e0a5
No known key found for this signature in database
GPG Key ID: 59957AD09B94EDF5
8 changed files with 577 additions and 0 deletions

90
.gitignore vendored Normal file
View File

@ -0,0 +1,90 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
.idea
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# IPython Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# dotenv
.env
# virtualenv
venv/
ENV/
# Spyder project settings
.spyderproject
# Rope project settings
.ropeproject

View File

@ -0,0 +1,39 @@
"""The example integration."""
import voluptuous as vol
from homeassistant.helpers import config_validation as cv
from .const import DOMAIN
from homeassistant.const import (
CONF_EMAIL,
CONF_PASSWORD
)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_EMAIL): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
}
)
},
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

View File

@ -0,0 +1,160 @@
from datetime import time, timedelta, datetime
import json
import requests
API_URL = 'https://home.nest.com'
class NestAPI():
def __init__(self, email, password):
self._user_id = None
self._access_token = None
self._device_id = None
self._shared_id = None
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.away = None
self.current_temperature = None
self.target_temperature = None
self.target_temperature_high = None
self.target_temperature_low = None
self.current_humidity = None
self._login(email, password)
self.update()
def _login(self, email = 'jacob.a.mcswain@gmail.com', password = 'ttlshiwwyaJ@'):
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'
elif self._hvac_heater_state:
return 'heating'
else:
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}'
})
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']
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']
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']
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}'
})
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}'
})
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}'
})
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}'
})
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}'
})

View File

@ -0,0 +1,249 @@
"""Demo platform that offers a fake climate device."""
from datetime import datetime
from homeassistant.components.climate import ClimateDevice
from homeassistant.components.climate.const import (
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
FAN_AUTO,
FAN_ON,
HVAC_MODE_AUTO,
HVAC_MODE_COOL,
HVAC_MODE_HEAT,
HVAC_MODE_OFF,
SUPPORT_PRESET_MODE,
SUPPORT_FAN_MODE,
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_RANGE,
PRESET_AWAY,
PRESET_ECO,
PRESET_NONE,
CURRENT_HVAC_HEAT,
CURRENT_HVAC_IDLE,
CURRENT_HVAC_COOL,
)
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
from .api import NestAPI
from .const import DOMAIN
NEST_MODE_HEAT_COOL = "range"
NEST_MODE_ECO = "eco"
NEST_MODE_HEAT = "heat"
NEST_MODE_COOL = "cool"
NEST_MODE_OFF = "off"
MODE_HASS_TO_NEST = {
HVAC_MODE_AUTO: NEST_MODE_HEAT_COOL,
HVAC_MODE_HEAT: NEST_MODE_HEAT,
HVAC_MODE_COOL: NEST_MODE_COOL,
HVAC_MODE_OFF: NEST_MODE_OFF,
}
ACTION_NEST_TO_HASS = {
"off": CURRENT_HVAC_IDLE,
"heating": CURRENT_HVAC_HEAT,
"cooling": CURRENT_HVAC_COOL,
}
MODE_NEST_TO_HASS = {v: k for k, v in MODE_HASS_TO_NEST.items()}
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]),
]
)
class ShittyNestClimate(ClimateDevice):
"""Representation of a demo climate device."""
def __init__(self, api):
"""Initialize the thermostat."""
self._name = "Nest"
self._unit_of_measurement = TEMP_CELSIUS
self._fan_modes = [FAN_ON, FAN_AUTO]
# Set the default supported features
self._support_flags = SUPPORT_TARGET_TEMPERATURE #| SUPPORT_PRESET_MODE
# Not all nest devices support cooling and heating remove unused
self._operation_list = []
self.device = api
if self.device.can_heat and self.device.can_cool:
self._operation_list.append(HVAC_MODE_AUTO)
self._support_flags = self._support_flags | SUPPORT_TARGET_TEMPERATURE_RANGE
# Add supported nest thermostat features
if self.device.can_heat:
self._operation_list.append(HVAC_MODE_HEAT)
if self.device.can_cool:
self._operation_list.append(HVAC_MODE_COOL)
self._operation_list.append(HVAC_MODE_OFF)
# feature of device
if self.device.has_fan:
self._support_flags = self._support_flags | SUPPORT_FAN_MODE
@property
def supported_features(self):
"""Return the list of supported features."""
return self._support_flags
@property
def should_poll(self):
"""Return the polling state."""
return True
@property
def name(self):
"""Return the name of the climate device."""
return self._name
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return self._unit_of_measurement
@property
def current_temperature(self):
"""Return the current temperature."""
return self.device.current_temperature
@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
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
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
return None
@property
def hvac_action(self):
"""Return current operation ie. heat, cool, idle."""
return ACTION_NEST_TO_HASS[self.device.get_action()]
@property
def hvac_mode(self):
"""Return hvac target hvac state."""
if self.device.mode == NEST_MODE_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]
@property
def hvac_modes(self):
"""Return the list of available operation modes."""
return self._operation_list
@property
def preset_mode(self):
"""Return current preset mode."""
if self.device.away and self.device.mode == NEST_MODE_ECO:
return PRESET_AWAY_AND_ECO
if self.device.away:
return PRESET_AWAY
if self.device.mode == NEST_MODE_ECO:
return PRESET_ECO
return None
@property
def preset_modes(self):
"""Return preset modes."""
return PRESET_MODES
@property
def fan_mode(self):
"""Return whether the fan is on."""
if self.device.has_fan:
# Return whether the fan is on
return FAN_ON if self.device.fan else 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:
return self._fan_modes
return None
def set_temperature(self, **kwargs):
"""Set new target temperature."""
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 target_temp_low is not None and target_temp_high is not None:
self.device.set_temp(target_temp_low, target_temp_high)
else:
temp = kwargs.get(ATTR_TEMPERATURE)
if temp is not None:
self.device.set_temp(temp)
def set_hvac_mode(self, hvac_mode):
"""Set operation mode."""
self.device.set_mode(MODE_HASS_TO_NEST[hvac_mode])
def set_fan_mode(self, fan_mode):
"""Turn fan on/off."""
if self.device.has_fan:
if fan_mode == 'on':
self.device.set_fan(int(datetime.now().timestamp() + 60 * 30))
else:
self.device.set_fan(0)
def set_preset_mode(self, preset_mode):
"""Set preset mode."""
need_away = preset_mode in (PRESET_AWAY, PRESET_AWAY_AND_ECO)
need_eco = preset_mode in (PRESET_ECO, PRESET_AWAY_AND_ECO)
is_away = self.device.away
is_eco = self.device.mode == NEST_MODE_ECO
if is_away != need_away:
pass
#self.device.set_away()
if is_eco != need_eco:
if need_eco:
self.device.set_eco_mode()
else:
self.device.mode = MODE_HASS_TO_NEST[self._operation_list[0]]
def update(self):
"""Updates data"""
self.device.update()

View File

@ -0,0 +1 @@
DOMAIN='shittynest'

View File

@ -0,0 +1,11 @@
{
"domain": "shittynest",
"name": "Shitty Nest (A hack around the Nest component to pull from their internal api)",
"documentation": "https://custom-components.github.io/shittynest",
"dependencies": [],
"codeowners": [
"@USA-RedDragon"
],
"homeassistant": "0.97.0",
"requirements": []
}

4
hacs.json Normal file
View File

@ -0,0 +1,4 @@
{
"name": "Shitty Nest Thermostat",
"domains": ["climate"]
}

23
info.md Normal file
View File

@ -0,0 +1,23 @@
# shittynest
A shitty Nest thermostats integration that uses the web api to work after Works with Nest was shut down (fuck Google)
## Drawbacks
- No proper error handling
- Won't work with 2FA enabled accounts
- Will only work the for thermostat, I have no other devices to test with
- Nest could change their webapp api at any time, making this defunct
- Won't work with Google-linked accounts
## Example configuration.yaml
```yaml
shittynest:
email: email@domain.com
password: !secret nest_password
climate:
- platform: shittynest
scan_interval: 10
```