Skip to content

Commit 3ced76d

Browse files
committed
Merge branch 'first_revision'
2 parents 03dbb32 + 6d33fd3 commit 3ced76d

5 files changed

Lines changed: 196 additions & 44 deletions

File tree

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1-
# python-tahoma-api
1+
# Somfy TaHoma
2+
3+
An updated and async version of the original [tahoma-api](https://github.com/philklei/tahoma-api) by [@philklei](https://github.com/philklei). The aim of this wrapper is to offer an easy to consume Python wrapper for the internal API's used by tahomalink.com.
4+
5+
Somfy TaHoma has an official API, which can be consumed via the [Somfy-open-api](https://github.com/tetienne/somfy-open-api). Unfortunately only a few device classes are supported via the official API, thus the need for this wrapper.
6+
7+
This package is written for the Home Assistant [ha-tahoma](https://github.com/iMicknl/ha-tahoma) integration, but could be used by any Python project interacting with Somfy TaHoma devices.

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
version="0.1.0",
99
author="Mick Vleeshouwer",
1010
author_email="mick@imick.nl",
11-
description="Python wrapper to interact with SagemCom F@st routers via internal API's.",
11+
description="Python wrapper to interact with internal Somfy TaHama API",
1212
long_description=long_description,
1313
long_description_content_type="text/markdown",
1414
url="https://github.com/iMicknl/python-tahoma-api",

tahoma_api/client.py

Lines changed: 97 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@
1010
import json
1111

1212
from .exceptions import *
13+
from .models import *
14+
15+
API_URL = "https://tahomalink.com/enduser-mobile-web/enduserAPI/" # /doc for API doc
1316

14-
API_URL = 'https://tahomalink.com/enduser-mobile-web/enduserAPI/' # /doc for API doc
1517

1618
class TahomaClient(object):
1719
""" Interface class for the Tahoma API """
1820

19-
def __init__(self, username, password):
21+
def __init__(self, username, password, api_url=API_URL):
2022
"""
2123
Constructor
2224
@@ -26,77 +28,133 @@ def __init__(self, username, password):
2628

2729
self.username = username
2830
self.password = password
31+
self.api_url = api_url
2932

30-
self.__roles = []
33+
self.__cookies = None
34+
self.__devices = None
35+
self.__roles = None
3136

3237
async def login(self):
33-
34-
payload = {
35-
'userId': self.username,
36-
'userPassword': self.password
37-
}
38+
39+
payload = {"userId": self.username, "userPassword": self.password}
3840

3941
async with aiohttp.ClientSession() as session:
40-
async with session.post(API_URL + 'login', data=payload) as response:
42+
async with session.post(self.api_url + "login", data=payload) as response:
4143

4244
result = await response.json()
4345

4446
# 401
4547
# {'errorCode': 'AUTHENTICATION_ERROR', 'error': 'Bad credentials'}
48+
# {'errorCode': 'AUTHENTICATION_ERROR', 'error': 'Your setup cannot be accessed through this application'}
49+
if response.status == 401:
50+
if result["errorCode"] == "AUTHENTICATION_ERROR":
51+
52+
if "Too many requests" in result["error"]:
53+
print(result["error"])
54+
raise Exception
55+
56+
if (
57+
"Your setup cannot be accessed through this application"
58+
in result["error"]
59+
):
60+
print(result["error"])
61+
62+
if "Bad credentials" in result["error"]:
63+
print(result["error"])
64+
65+
print(result["error"])
66+
67+
return False # todo throw error
4668

4769
# 401
4870
# {'errorCode': 'AUTHENTICATION_ERROR', 'error': 'Too many requests, try again later : login with xxx@xxx.tld'}
4971
# TODO Add retry logic on too many requests + for debug, log requests + timespans
5072

5173
# 200
5274
# {'success': True, 'roles': [{'name': 'ENDUSER'}]}
53-
if (response.status is 200):
54-
if result['success'] == True:
55-
self.__roles = result['roles']
75+
if response.status == 200:
76+
if result["success"] == True:
77+
self.__roles = result["roles"]
5678
self.__cookies = response.cookies
5779

5880
return True
5981

82+
# Temp fallbacks
6083
print(response.status)
6184
print(result)
6285

63-
async def get_devices(self):
64-
65-
cookies = self.__cookies
86+
async def get_devices(self, refresh=False) -> List[Device]:
87+
if self.__devices and refresh == False:
88+
return self._devices
6689

67-
async with aiohttp.ClientSession() as session:
68-
async with session.get(API_URL + 'setup/devices', cookies=cookies) as response:
90+
response = await self.__make_http_request("GET", "setup/devices")
6991

70-
print(response.status)
71-
print(response)
72-
73-
result = await response.json()
92+
devices = [Device(**d) for d in response]
93+
self.__devices = devices
7494

75-
print(result)
76-
# 401
77-
# {'errorCode': 'AUTHENTICATION_ERROR', 'error': 'Bad credentials'}
95+
return devices
7896

79-
# {'success': True, 'roles': [{'name': 'ENDUSER'}]}
80-
if (response.status is 200):
81-
if result["success"] == True:
82-
print(result)
97+
async def register_event_listener(self) -> str:
98+
"""
99+
Register a new setup event listener on the current session and return a new listener id.
100+
Only one listener may be registered on a given session.
101+
Registering an new listener will invalidate the previous one if any.
102+
Note that registering an event listener drastically reduces the session timeout : listening sessions are expected to call the /events/{listenerId}/fetch API on a regular basis.
103+
"""
104+
response = await self.__make_http_request("POST", "events/register")
105+
listener_id = response.get("id")
83106

84-
# TODO Save cookies
107+
return listener_id
85108

86-
async def get_states(self):
87-
88-
cookies = self.__cookies
109+
async def fetch_event_listener(self, listener_id: str) -> List[Any]:
110+
"""
111+
Fetch new events from a registered event listener. Fetched events are removed from the listener buffer. Return an empty response if no event is available.
112+
Per-session rate-limit : 1 calls per 1 SECONDS period for this particular operation (polling)
113+
"""
114+
response = await self.__make_http_request("POST", f"events/{listener_id}/fetch")
89115

90-
async with aiohttp.ClientSession() as session:
91-
async with session.get(API_URL + 'setup/devices/states', cookies=cookies) as response:
116+
return response
92117

93-
print(response.status)
94-
result = await response.json()
118+
async def execute_action_group(
119+
self, actions: [Command], label="python-tahoma-api", priority=False
120+
) -> List[Any]:
121+
"""
122+
Execute a non-persistent action group
123+
The executed action group does not have to be persisted on the server before use.
124+
Per-session rate-limit : 50 calls per 24 HOURS period for all operations of the same category (exec)
125+
"""
126+
payload = {"label": label, "actions": actions}
127+
response = await self.__make_http_request("POST", f"exec/apply", payload)
95128

96-
print(result)
97-
129+
return response
98130

131+
async def __make_http_request(
132+
self, method: str, endpoint: str, payload: Optional[Any] = None
133+
) -> Any:
134+
"""Make a request to the TaHoma API"""
135+
cookies = self.__cookies
136+
supported_methods = ["GET", "POST"]
99137

100-
138+
if method not in supported_methods:
139+
raise Exception
101140

141+
async with aiohttp.ClientSession() as session:
142+
if method == "GET":
143+
async with session.get(
144+
self.api_url + endpoint, cookies=cookies
145+
) as response:
146+
result = await response.json()
147+
148+
if method == "POST":
149+
async with session.post(
150+
self.api_url + endpoint, cookies=cookies, data=payload
151+
) as response:
152+
result = await response.json()
153+
154+
if response.status == 200:
155+
return result
156+
157+
if response.status > 400 and response.status < 500:
158+
# implement retry logic
159+
print("TODO")
102160

tahoma_api/models.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from typing import Any, Dict, List, Optional, Union
2+
3+
# TODO Rewrite camelCase to snake_case
4+
class Device:
5+
__slots__ = (
6+
"id",
7+
"creationTime",
8+
"lastUpdateTime",
9+
"label",
10+
"deviceURL",
11+
"shortcut",
12+
"controllableName",
13+
"definition",
14+
"states",
15+
"dataProperties",
16+
"available",
17+
"enabled",
18+
"widgetName",
19+
"widget",
20+
"uiClass",
21+
"qualifiedName",
22+
"type",
23+
)
24+
25+
def __init__(
26+
self,
27+
*,
28+
label: str,
29+
deviceURL: str,
30+
controllableName: str,
31+
# definition: Dict[List[Any]],
32+
states: List[Dict[str, Any]],
33+
dataProperties: Optional[List[Dict[str, Any]]] = None,
34+
widgetName: Optional[str] = None,
35+
uiClass: str,
36+
qualifiedName: Optional[str] = None,
37+
type: str,
38+
**kwargs: Any
39+
):
40+
self.id = deviceURL
41+
self.deviceURL = deviceURL
42+
self.label = label
43+
self.controllableName = controllableName
44+
self.states = [State(**s) for s in states]
45+
46+
47+
class StateDefinition:
48+
__slots__ = (
49+
"qualifiedName",
50+
"type",
51+
"values",
52+
)
53+
54+
def __init__(
55+
self, qualifiedName: str, type: str, values: Optional[str], **kwargs: Any
56+
):
57+
self.qualifiedName = qualifiedName
58+
self.type = type
59+
self.values = values
60+
61+
62+
class CommandDefinition:
63+
__slots__ = (
64+
"commandName",
65+
"nparams",
66+
)
67+
68+
def __init__(self, commandName: str, nparams: int, **kwargs: Any):
69+
self.commandName = commandName
70+
self.nparams = nparams
71+
72+
73+
class State:
74+
__slots__ = "name", "value", "type"
75+
76+
def __init__(self, name: str, value: str, type: str, **kwargs: Any):
77+
self.name = name
78+
self.value = value
79+
self.type = type

test.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
11
import asyncio
22
from tahoma_api import TahomaClient
3+
import time
34

45
username = ""
56
password = ""
67

8+
79
async def main():
810
client = TahomaClient(username, password)
9-
11+
1012
try:
1113
login = await client.login()
12-
devices = await client.get_states()
14+
devices = await client.get_devices()
15+
16+
for device in devices:
17+
print(f"{device.label} ({device.id})")
1318

14-
print(devices)
19+
listener_id = await client.register_event_listener()
1520

21+
while True:
22+
events = await client.fetch_event_listener(listener_id)
23+
print(events)
24+
time.sleep(2)
1625

1726
except Exception as exception:
1827
print(exception)

0 commit comments

Comments
 (0)