Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hops rework #13

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ pytado.egg-info

PyCharms
.idea/

test_credentials.py
3 changes: 2 additions & 1 deletion AUTHORS
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
Author: Chris Jewell <chrism0dwk@gmail.com>
Modified: Gareth Jeanne <contact@garethjeanne.co.uk>
Wolfgang Malgadey <w.malgadey@gmail.com>
Wolfgang Malgadey <w.malgadey@gmail.com>
Petr Svoboda <svobodpe@gmail.com>
8 changes: 4 additions & 4 deletions PyTado/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ def log_in(email, password):

def get_me(args):
t = log_in(args.email, args.password)
me = tado_client.getMe(t)
me = tado_client.get_me(t)
print(me)

def get_state(args):
t = log_in(args.email, args.password)
zone = tado_client.getState(t,int(args.zone))
zone = tado_client.get_state(t, int(args.zone))
print(zone)

def get_capabilities(args):
t = log_in(args.email, args.password)
capabilities = tado_client.getCapabilities(t,int(args.zone))
capabilities = tado_client.get_capabilities(t, int(args.zone))
print(capabilities)

def main():
Expand All @@ -39,7 +39,7 @@ def main():
# Required flags go here.
required_flags.add_argument('--email',
required=True,
help=('Tado username in the form of an email address.'))
help='Tado username in the form of an email address.')
required_flags.add_argument('--password',
required=True,
help='Tado password.')
Expand Down
254 changes: 145 additions & 109 deletions PyTado/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ class Tado:
headers = {'Referer' : 'https://my.tado.com/'}
api2url = 'https://my.tado.com/api/v2/homes/'
mobi2url = 'https://my.tado.com/mobile/1.9/'
hops2url = 'https://hops.tado.com/homes'
refresh_token = ''
refresh_at = datetime.datetime.now() + datetime.timedelta(minutes=5)

# 'Private' methods for use in class, Tado mobile API V1.9.
def _mobile_apiCall(self, cmd):
# pylint: disable=C0103
def _mobile_api_call(self, cmd):

self._refresh_token()

Expand All @@ -53,8 +53,7 @@ def _mobile_apiCall(self, cmd):
return data

# 'Private' methods for use in class, Tado API V2.
def _apiCall(self, cmd, method="GET", data=None, plain=False):
# pylint: disable=C0103
def _api_call(self, cmd, method="GET", data=None, plain=False):

self._refresh_token()

Expand Down Expand Up @@ -91,8 +90,45 @@ def _apiCall(self, cmd, method="GET", data=None, plain=False):
data = json.loads(str_response)
return data

def _setOAuthHeader(self, data):
# pylint: disable=C0103

# 'Private' methods for use in class, Tado API V2.
def _hops_api_call(self, cmd, method="GET", data=None, plain=False):

self._refresh_token()

headers = self.headers

if data is not None:
if plain:
headers['Content-Type'] = 'text/plain;charset=UTF-8'
else:
headers['Content-Type'] = 'application/json;charset=UTF-8'
headers['Mime-Type'] = 'application/json;charset=UTF-8'
data = json.dumps(data).encode('utf8')

if self._debugCalls:
_LOGGER.debug("api call: %s: %s, headers %s, data %s",
method, cmd, headers, data)
url = f'{self.hops2url}/{self.id}/{cmd}'
req = urllib.request.Request(url,
headers=headers,
method=method,
data=data)

response = self.opener.open(req)

if self._debugCalls:
_LOGGER.debug("api call: %s: %s, response %s",
method, cmd, response)

str_response = response.read().decode('utf-8')
if str_response is None or str_response == "":
return

data = json.loads(str_response)
return data

def _set_o_auth_header(self, data):

access_token = data['access_token']
expires_in = float(data['expires_in'])
Expand Down Expand Up @@ -128,11 +164,10 @@ def _refresh_token(self):
response = self.opener.open(req)
str_response = response.read().decode('utf-8')

self._setOAuthHeader(json.loads(str_response))
self._set_o_auth_header(json.loads(str_response))
return response

def _loginV2(self, username, password):
# pylint: disable=C0103
def _login_v2(self, username, password):

headers = self.headers
headers['Content-Type'] = 'application/json'
Expand All @@ -154,149 +189,150 @@ def _loginV2(self, username, password):
response = self.opener.open(req)
str_response = response.read().decode('utf-8')

self._setOAuthHeader(json.loads(str_response))
self._set_o_auth_header(json.loads(str_response))
return response

def setDebugging(self, debugCalls):
self._debugCalls = debugCalls
def set_debugging(self, debug_calls):
self._debugCalls = debug_calls
return self._debugCalls

# Public interface
def getMe(self):
def get_me(self) -> dict:
"""Gets home information."""
# pylint: disable=C0103

url = 'https://my.tado.com/api/v2/me'
req = urllib.request.Request(url, headers=self.headers)
response = self.opener.open(req)
str_response = response.read().decode('utf-8')
data = json.loads(str_response)
return data

def getDevices(self):
"""Gets device information."""
# pylint: disable=C0103

cmd = 'devices'
data = self._apiCall(cmd)
def get_rooms_and_devices(self) -> dict:
"""Gets room and device information."""
cmd = 'roomsAndDevices'
data = self._hops_api_call(cmd)
return data

def getZones(self):
"""Gets zones information."""
# pylint: disable=C0103

cmd = 'zones'
data = self._apiCall(cmd)
def get_rooms(self) -> list[dict]:
"""Gets rooms information."""
cmd = 'rooms'
data = self._hops_api_call(cmd)
return data

def getState(self, zone):
"""Gets current state of Zone zone."""
# pylint: disable=C0103

cmd = 'zones/%i/state' % zone
data = self._apiCall(cmd)
def get_room(self, room_id: int) -> dict:
"""Gets room information."""
cmd = f'rooms/{room_id}'
data = self._hops_api_call(cmd)
return data

def getCapabilities(self, zone):
"""Gets current capabilities of Zone zone."""
# pylint: disable=C0103

cmd = 'zones/%i/capabilities' % zone
data = self._apiCall(cmd)
def get_room_day_report(self, room_id: int, date: datetime.date) -> dict:
"""Gets room day report with temperature, humidity, heating and weather information."""
cmd = f'zones/{room_id}/dayReport?date={date.strftime('%Y-%m-%d')}'
data = self._api_call(cmd)
return data

def getClimate(self, zone):
"""Gets temp (centigrade) and humidity (% RH) for Zone zone."""
# pylint: disable=C0103

data = self.getState(zone)['sensorDataPoints']
return {'temperature' : data['insideTemperature']['celsius'],
'humidity' : data['humidity']['percentage']}

def getWeather(self):
"""Gets outside weather data"""
# pylint: disable=C0103

cmd = 'weather'
data = self._apiCall(cmd)
def get_air_comfort(self) -> dict:
"""Gets air comfort information with current temperature, humidity and open window detection."""
cmd = f'airComfort'
data = self._hops_api_call(cmd)
return data

def getAppUsers(self):
"""Gets getAppUsers data"""
# pylint: disable=C0103
def get_tado_mode(self) -> dict:
"""Gets tado mode."""
cmd = f'state'
data = self._api_call(cmd)
return data['presence']

cmd = 'getAppUsers'
data = self._mobile_apiCall(cmd)
def set_home(self) -> None:
"""Sets tado mode to home."""
cmd = f'presenceLock'
data = self._api_call(cmd, method="PUT", data={"homePresence": "HOME"})
return data

def getAppUsersRelativePositions(self):
"""Gets getAppUsersRelativePositions data"""
# pylint: disable=C0103

cmd = 'getAppUsersRelativePositions'
data = self._mobile_apiCall(cmd)
def set_away(self) -> None:
"""Sets tado mode to away."""
cmd = f'presenceLock'
data = self._api_call(cmd, method="PUT", data={"homePresence": "AWAY"})
return data

def resetZoneOverlay(self, zone):
"""Delete current overlay"""
# pylint: disable=C0103

cmd = 'zones/%i/overlay' % zone
data = self._apiCall(cmd, "DELETE", {}, True)
def boost_heating(self) -> None:
"""Boost mode, expires after 30 minutes."""
cmd = f'quickActions/boost'
data = self._hops_api_call(cmd, method="POST")
return data

def setZoneOverlay(self, zone, overlayMode, setTemp=None, duration=None, deviceType='HEATING', power="ON", mode=None):
"""set current overlay for a zone"""
# pylint: disable=C0103
def disable_heating(self) -> None:
"""Sets all rooms off, frost protection."""
cmd = f'quickActions/allOff'
data = self._hops_api_call(cmd, method="POST")
return data

cmd = 'zones/%i/overlay' % zone
def resume_schedule(self) -> None:
"""Resumes regular schedule for all rooms, undo boost, disable heating and manual settings."""
cmd = f'quickActions/resumeSchedule'
data = self._hops_api_call(cmd, method="POST")
return data

post_data = {
"setting" : {},
"termination" : {}
}
def get_room_schedule(self, room_id: int) -> dict:
"""Get room weekly schedule."""
cmd = f'rooms/{room_id}/schedule'
data = self._hops_api_call(cmd, method="GET")
return data

if setTemp is None:
post_data["setting"] = {
"type": deviceType,
"power": power
}
elif mode is not None:
post_data["setting"] = {
"type": deviceType,
"power": power,
"mode": mode,
"temperature":{
"celsius": setTemp
}
}
else:
post_data["setting"] = {
"type": deviceType,
"power": power,
"temperature":{
"celsius": setTemp
def set_room_schedule(self, room_id: int, schedule: dict) -> dict:
"""Get room weekly schedule. Can set on day at a time. Sample payload:
{
"dayType": "SUNDAY",
"daySchedule": [
{
"start": "00:00",
"end": "07:00",
"dayType": "SUNDAY",
"setting": {
"power": "ON",
"temperature": {
"value": 19
}
}
},
{
"start": "07:00",
"end": "22:00",
"dayType": "SUNDAY",
"setting": {
"power": "ON",
"temperature": {
"value": 21
}
}
},
{
"start": "22:00",
"end": "24:00",
"dayType": "SUNDAY",
"setting": {
"power": "ON",
"temperature": {
"value": 19
}
}
}
}

post_data["termination"] = {"type" : overlayMode}

if duration is not None:
post_data["termination"]["durationInSeconds"] = duration

data = self._apiCall(cmd, "PUT", post_data)
]
}
"""
cmd = f'rooms/{room_id}/schedule'
data = self._hops_api_call(cmd, method="POST", data=schedule)
return data


# Ctor
def __init__(self, username, password):
"""Performs login and save session cookie."""
# HTTPS Interface

# pylint: disable=C0103
cj = CookieJar()

self.opener = urllib.request.build_opener(
urllib.request.HTTPCookieProcessor(cj),
urllib.request.HTTPSHandler())
self._loginV2(username, password)
self.id = self.getMe()['homes'][0]['id']
self._login_v2(username, password)
self.id = self.get_me()['homes'][0]['id']
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Example basic usage

>>> from PyTado.interface import Tado
>>> t = Tado('my@username.com', 'mypassword')
>>> climate = t.getClimate(zone=1)
>>> climate = t.get_room_schedule(1)

Development
-----------
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@

pytest
Loading