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:
commit
262622e0a5
90
.gitignore
vendored
Normal file
90
.gitignore
vendored
Normal 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
|
39
custom_components/shittynest/__init__.py
Normal file
39
custom_components/shittynest/__init__.py
Normal 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
|
160
custom_components/shittynest/api.py
Normal file
160
custom_components/shittynest/api.py
Normal 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}'
|
||||
})
|
249
custom_components/shittynest/climate.py
Normal file
249
custom_components/shittynest/climate.py
Normal 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()
|
||||
|
1
custom_components/shittynest/const.py
Normal file
1
custom_components/shittynest/const.py
Normal file
@ -0,0 +1 @@
|
||||
DOMAIN='shittynest'
|
11
custom_components/shittynest/manifest.json
Normal file
11
custom_components/shittynest/manifest.json
Normal 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
4
hacs.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "Shitty Nest Thermostat",
|
||||
"domains": ["climate"]
|
||||
}
|
23
info.md
Normal file
23
info.md
Normal 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
|
||||
```
|
Loading…
x
Reference in New Issue
Block a user