Skip to content

Commit 836be90

Browse files
committed
finish basic implementation
1 parent 458391c commit 836be90

20 files changed

Lines changed: 985 additions & 175 deletions

blueconnect/__init__.py

Lines changed: 45 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -1,191 +1,72 @@
11
"""Unofficial python library for the Blue Riiot Blue Connect API."""
22

3-
43
import asyncio
5-
import time
6-
import json
74
import logging
5+
from typing import List
86

9-
import aiohttp
10-
from aws_request_signer import AwsRequestSigner
7+
from .api import BlueConnectApi
8+
from .models import (
9+
BlueDevice,
10+
SwimmingPool,
11+
SwimmingPoolFeedMessage,
12+
SwimmingPoolMeasurement,
13+
TemperatureUnit,
14+
)
1115

12-
AWS_REGION = "eu-west-1"
13-
AWS_SERVICE = "execute-api"
14-
BASE_HEADERS = {
15-
"User-Agent": "BlueConnect/3.2.1",
16-
"Accept-Language": "en-DK;q=1.0, da-DK;q=0.9",
17-
"Accept": "*/*"
18-
}
19-
BASE_URL = "https://api.riiotlabs.com/prod/"
2016
LOGGER = logging.getLogger()
2117

2218

23-
class BlueConnectApi():
24-
"""Class that holds the connection to the Blue Connect API."""
25-
_username = None
26-
_password = None
27-
_language = None
28-
_token_info = {}
29-
_loop = None
30-
_http_session = None
31-
_user_info = {}
32-
_swimming_pool_info = {}
33-
_swimming_pool_status = {}
34-
_swimming_pool_feed = {}
35-
_swimming_pool_ph = {}
36-
_swimming_pool_orp = {}
37-
_swimming_pool_temp = {}
38-
_swimming_pool_device = {}
19+
class BlueConnectSimpleAPI:
20+
"""Class that provides a common structure around the Blue Connect API for a single/main swimming pool."""
3921

4022
def __init__(self, username: str, password: str, language: str = "en") -> None:
4123
"""Inititialize the api connection, a valid username and password must be provided."""
42-
self._username = username
43-
self._password = password
24+
self._api = BlueConnectApi(username, password)
4425
self._language = language
45-
self._loop = asyncio.get_event_loop()
46-
self._http_session = aiohttp.ClientSession(
47-
loop=self._loop, connector=aiohttp.TCPConnector()
48-
)
49-
50-
def close(self):
51-
"""Close connection to the api."""
52-
asyncio.create_task(self.close_async())
26+
self._temperature_unit = None
27+
self._pool_info = None
28+
self._pool_feed_message = None
29+
self._pool_blue_device = None
30+
self._pool_measurements = []
5331

54-
async def close_async(self):
32+
def close(self) -> None:
5533
"""Close connection to the api."""
56-
await self._http_session.close()
34+
self._api.close()
5735

58-
async def fetch_data(self):
36+
async def fetch_data(self) -> None:
5937
"""Fetch latest state from API."""
60-
self._user_info = await self.get_user_info()
61-
self._swimming_pool_info = await self.get_swimming_pool_info(self.main_swimming_pool_id)
62-
self._swimming_pool_status = await self.get_swimming_pool_status(self.main_swimming_pool_id)
63-
self._swimming_pool_feed = await self.get_swimming_pool_feed(self.main_swimming_pool_id)
64-
self._swimming_pool_device = await self.get_swimming_pool_blue_device(self.main_swimming_pool_id)
65-
# get levels from status task infos
66-
for task in self.swimming_pool_status["tasks"]:
67-
if task["task_identifier"].startswith("ORP_"):
68-
self._swimming_pool_orp = json.loads(task["data"])
69-
if task["task_identifier"].startswith("TEMPERATURE_"):
70-
self._swimming_pool_temp = json.loads(task["data"])
71-
if task["task_identifier"].startswith("PH_"):
72-
self._swimming_pool_ph = json.loads(task["data"])
73-
74-
@property
75-
def main_swimming_pool_id(self):
76-
"""Return ID of the main swimming pool."""
77-
return self.user_preferences.get("main_swimming_pool_id")
78-
79-
@property
80-
def swimming_pool_name(self):
81-
"""Return name of the main swimming pool."""
82-
return self.swimming_pool_info.get("name")
83-
84-
@property
85-
def temperature_unit(self):
86-
"""Return temperature_unit preference of the logged in user."""
87-
return self.user_preferences.get("display_temperature_unit")
88-
89-
@property
90-
def swimming_pool_info(self):
91-
"""Return info for the main swimming pool."""
92-
return self._swimming_pool_info
93-
94-
@property
95-
def swimming_pool_status(self):
96-
"""Return info for the main swimming pool."""
97-
return self._swimming_pool_status
98-
99-
@property
100-
def swimming_pool_feed(self):
101-
"""Return status feed for the main swimming pool."""
102-
return self._swimming_pool_feed
38+
user_info = await self._api.get_user()
39+
main_pool_id = user_info.user_preferences.main_swimming_pool_id
40+
self._temperature_unit = user_info.user_preferences.display_temperature_unit
41+
self._pool_info = await self._api.get_swimming_pool(main_pool_id)
42+
self._pool_feed_message = (await self._api.get_swimming_pool_feed(main_pool_id, self._language)).current_message
43+
blue_devices = await self._api.get_swimming_pool_blue_devices(main_pool_id)
44+
self._pool_blue_device = blue_devices[0] if blue_devices else None
45+
if self._pool_blue_device:
46+
self._pool_measurements = (await self._api.get_last_measurements(
47+
main_pool_id, self._pool_blue_device.serial)).measurements
10348

10449
@property
105-
def swimming_pool_device(self):
106-
"""Return Blue Connect device info for the main swimming pool."""
107-
return self._swimming_pool_device
50+
def pool(self) -> SwimmingPool:
51+
"""Return full details of the (main) swimming pool."""
52+
return self._pool_info
10853

10954
@property
110-
def user_info(self):
111-
"""Return info about logged in user."""
112-
return self._user_info
55+
def temperature_unit(self) -> TemperatureUnit:
56+
"""Return temperature unit of the temperature measurements."""
57+
return self._temperature_unit
11358

11459
@property
115-
def user_preferences(self):
116-
"""Return preferences of logged in user."""
117-
return self._user_info.get("userPreferences", {})
60+
def feed_message(self) -> SwimmingPoolFeedMessage:
61+
"""Return the (latest) feed/health message for the (main) swimming pool."""
62+
return self._pool_feed_message
11863

11964
@property
120-
def swimming_pool_ph(self):
121-
"""Return current PH info of the main swimming pool."""
122-
return self._swimming_pool_ph
65+
def blue_device(self) -> BlueDevice:
66+
"""Return Blue Connect device info for the (main) swimming pool."""
67+
return self._pool_blue_device
12368

12469
@property
125-
def swimming_pool_orp(self):
126-
"""Return current ORP info of the main swimming pool."""
127-
return self._swimming_pool_orp
128-
129-
@property
130-
def swimming_pool_temp(self):
131-
"""Return current Temperature info of the main swimming pool."""
132-
return self._swimming_pool_temp
133-
134-
async def get_user_info(self):
135-
"""Retrieve details of logged in user."""
136-
return await self.__get_data("user")
137-
138-
async def get_swimming_pool_info(self, swimming_pool_id: str):
139-
"""Retrieve details for a specific swimming pool."""
140-
return await self.__get_data(f"swimming_pool/{swimming_pool_id}")
141-
142-
async def get_swimming_pool_status(self, swimming_pool_id: str):
143-
"""Retrieve status for a specific swimming pool."""
144-
return await self.__get_data(f"swimming_pool/{swimming_pool_id}/status")
145-
146-
async def get_swimming_pool_blue_device(self, swimming_pool_id: str):
147-
"""Retrieve Blue device info for a specific swimming pool."""
148-
return await self.__get_data(f"swimming_pool/{swimming_pool_id}/blue")
149-
150-
async def get_swimming_pool_feed(self, swimming_pool_id: str):
151-
"""Retrieve feed for a specific swimming pool, defaults to user's main swimming pool."""
152-
return await self.__get_data(f"swimming_pool/{swimming_pool_id}/feed?lang={self._language}")
153-
154-
async def __get_credentials(self):
155-
"""Retrieve auth credentials by logging in with username/password."""
156-
if self._token_info and self._token_info["expires"] > time.time():
157-
# return cached credentials if still valid
158-
return self._token_info["credentials"]
159-
# perform log-in to get credentials
160-
url = BASE_URL + "user/login"
161-
async with self._http_session.post(
162-
url, json={"email": self._username, "password": self._password}
163-
) as response:
164-
if response.status != 200:
165-
LOGGER.exception(await response.text())
166-
return None
167-
result = await response.json()
168-
self._token_info = result
169-
self._token_info["expires"] = time.time() + 3500
170-
return result["credentials"]
171-
172-
async def __get_data(self, endpoint, params={}):
173-
"""Get data from api."""
174-
url = BASE_URL + endpoint
175-
headers = BASE_HEADERS.copy()
176-
# sign the request
177-
creds = await self.__get_credentials()
178-
if not creds:
179-
return None
180-
request_signer = AwsRequestSigner(
181-
AWS_REGION, creds["access_key"], creds["secret_key"], AWS_SERVICE
182-
)
183-
headers.update(
184-
request_signer.sign_with_headers("GET", url, headers)
185-
)
186-
headers["X-Amz-Security-Token"] = creds["session_token"]
187-
async with self._http_session.get(
188-
url, headers=headers, params=params, verify_ssl=False
189-
) as response:
190-
assert response.status == 200
191-
return await response.json()
70+
def measurements(self) -> List[SwimmingPoolMeasurement]:
71+
"""Return all last/current measurements for the (main) swimming pool."""
72+
return self._pool_measurements

blueconnect/api.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
"""Unofficial python library for the Blue Riiot Blue Connect API."""
2+
3+
import asyncio
4+
import logging
5+
import time
6+
from typing import List, Optional
7+
8+
import aiohttp
9+
from aws_request_signer import AwsRequestSigner
10+
11+
from .models import (
12+
BlueDevice,
13+
SwimmingPool,
14+
SwimmingPoolFeed,
15+
SwimmingPoolLastMeasurements,
16+
SwimmingPoolStatus,
17+
User,
18+
)
19+
20+
AWS_REGION = "eu-west-1"
21+
AWS_SERVICE = "execute-api"
22+
BASE_HEADERS = {
23+
"User-Agent": "BlueConnect/3.2.1",
24+
"Accept-Language": "en-DK;q=1.0, da-DK;q=0.9",
25+
"Accept": "*/*",
26+
}
27+
BASE_URL = "https://api.riiotlabs.com/prod/"
28+
LOGGER = logging.getLogger()
29+
30+
31+
class BlueConnectApi:
32+
"""Class that holds the connection to the Blue Connect API."""
33+
34+
def __init__(self, username: str, password: str) -> None:
35+
"""Inititialize the api connection, a valid username and password must be provided."""
36+
self._username = username
37+
self._password = password
38+
self._http_session = aiohttp.ClientSession(
39+
connector=aiohttp.TCPConnector()
40+
)
41+
self._token_info = {}
42+
43+
def close(self) -> None:
44+
"""Close connection to the api."""
45+
if asyncio.get_event_loop().is_running():
46+
asyncio.create_task(self.close_async())
47+
else:
48+
asyncio.get_event_loop().run_until_complete(self.close_async())
49+
50+
async def close_async(self) -> None:
51+
"""Close connection to the api."""
52+
await self._http_session.close()
53+
54+
async def get_user(self) -> User:
55+
"""Retrieve details of logged-in user."""
56+
data = await self.__get_data(f"user")
57+
return User.from_json(data)
58+
59+
async def get_blue_device(self, blue_device_serial: str) -> BlueDevice:
60+
"""Retrieve details for a specific blue device."""
61+
data = await self.__get_data(f"blue/{blue_device_serial}")
62+
return BlueDevice.from_json(data)
63+
64+
async def get_swimming_pools(self) -> List[SwimmingPool]:
65+
"""Retrieve all swimming pools."""
66+
data = await self.__get_data(f"swimming_pool")
67+
return [SwimmingPool.from_json(item["swimming_pool"]) for item in data["data"]]
68+
69+
async def get_swimming_pool(self, swimming_pool_id: str) -> SwimmingPool:
70+
"""Retrieve details for a specific swimming pool."""
71+
data = await self.__get_data(f"swimming_pool/{swimming_pool_id}")
72+
return SwimmingPool.from_json(data)
73+
74+
async def get_swimming_pool_status(self, swimming_pool_id: str) -> SwimmingPoolStatus:
75+
"""Retrieve status for a specific swimming pool."""
76+
data = await self.__get_data(f"swimming_pool/{swimming_pool_id}/status")
77+
return SwimmingPoolStatus.from_json(data)
78+
79+
async def get_swimming_pool_blue_devices(self, swimming_pool_id: str) -> List[BlueDevice]:
80+
"""Retrieve Blue devices for a specific swimming pool."""
81+
data = await self.__get_data(f"swimming_pool/{swimming_pool_id}/blue")
82+
result = []
83+
for item in data["data"]:
84+
blue_device = await self.get_blue_device(item["blue_device_serial"])
85+
result.append(blue_device)
86+
return result
87+
88+
async def get_swimming_pool_feed(self, swimming_pool_id: str, language: str = "en") -> SwimmingPoolFeed:
89+
"""Retrieve feed for a specific swimming pool."""
90+
data = await self.__get_data(
91+
f"swimming_pool/{swimming_pool_id}/feed?lang={language}"
92+
)
93+
return SwimmingPoolFeed.from_json(data)
94+
95+
async def get_last_measurements(self, swimming_pool_id: str, blue_device_serial: str) -> SwimmingPoolLastMeasurements:
96+
"""Retrieve last measurements for a specific swimming pool."""
97+
data = await self.__get_data(
98+
f"swimming_pool/{swimming_pool_id}/blue/{blue_device_serial}/lastMeasurements?mode=blue_and_strip"
99+
)
100+
return SwimmingPoolLastMeasurements.from_json(data)
101+
102+
async def __get_credentials(self) -> dict:
103+
"""Retrieve auth credentials by logging in with username/password."""
104+
if self._token_info and self._token_info["expires"] > time.time():
105+
# return cached credentials if still valid
106+
return self._token_info["credentials"]
107+
# perform log-in to get credentials
108+
url = BASE_URL + "user/login"
109+
async with self._http_session.post(
110+
url, json={"email": self._username, "password": self._password}
111+
) as response:
112+
if response.status != 200:
113+
error_msg = await response.text()
114+
raise Exception("Error logging in user: %s" % error_msg)
115+
result = await response.json()
116+
self._token_info = result
117+
self._token_info["expires"] = time.time() + 3500
118+
return result["credentials"]
119+
120+
async def __get_data(self, endpoint: str, params: dict = None) -> Optional[dict]:
121+
"""Get data from api."""
122+
if params is None:
123+
params = {}
124+
url = BASE_URL + endpoint
125+
headers = BASE_HEADERS.copy()
126+
# sign the request
127+
creds = await self.__get_credentials()
128+
request_signer = AwsRequestSigner(
129+
AWS_REGION, creds["access_key"], creds["secret_key"], AWS_SERVICE
130+
)
131+
headers.update(request_signer.sign_with_headers("GET", url, headers))
132+
headers["X-Amz-Security-Token"] = creds["session_token"]
133+
async with self._http_session.get(
134+
url, headers=headers, params=params, verify_ssl=False
135+
) as response:
136+
if response.status != 200:
137+
error_msg = await response.text()
138+
raise Exception("Error while retrieving data for endpoint %s: %s" % (endpoint, error_msg))
139+
return await response.json()

0 commit comments

Comments
 (0)