Skip to content

Commit 537222b

Browse files
committed
initial commit
1 parent c4bf285 commit 537222b

11 files changed

Lines changed: 517 additions & 0 deletions

File tree

.github/release-drafter.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
change-template: "- #$NUMBER - $TITLE (@$AUTHOR)"
2+
categories:
3+
- title: "⚠ Breaking Changes"
4+
labels:
5+
- "breaking change"
6+
template: |
7+
## What’s Changed
8+
9+
$CHANGES
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Publish releases to PyPI
2+
3+
on:
4+
release:
5+
types: [published, prereleased]
6+
7+
jobs:
8+
build-and-publish:
9+
name: Builds and publishes releases to PyPI
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@master
13+
- name: Set up Python 3.7
14+
uses: actions/setup-python@v1
15+
with:
16+
python-version: 3.7
17+
- name: Install wheel
18+
run: >-
19+
pip install wheel
20+
- name: Build
21+
run: >-
22+
python3 setup.py sdist bdist_wheel
23+
- name: Publish release to PyPI
24+
uses: pypa/gh-action-pypi-publish@master
25+
with:
26+
user: __token__
27+
password: ${{ secrets.PYPI_TOKEN }}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name: Release Drafter
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
8+
jobs:
9+
update_release_draft:
10+
runs-on: ubuntu-latest
11+
steps:
12+
# Drafts your next Release notes as Pull Requests are merged into "master"
13+
- uses: release-drafter/release-drafter@v5
14+
env:
15+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/test.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# This workflow will install Python dependencies, run tests and lint
2+
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
3+
4+
name: Test
5+
6+
on:
7+
push:
8+
branches: [master]
9+
pull_request:
10+
branches: [master]
11+
12+
jobs:
13+
build:
14+
runs-on: ubuntu-latest
15+
strategy:
16+
matrix:
17+
python-version: [3.7, 3.8]
18+
19+
steps:
20+
- uses: actions/checkout@v2
21+
- name: Set up Python ${{ matrix.python-version }}
22+
uses: actions/setup-python@v1
23+
with:
24+
python-version: ${{ matrix.python-version }}
25+
- name: Install dependencies
26+
run: |
27+
python -m pip install --upgrade pip
28+
pip install tox tox-gh-actions
29+
- name: Test with tox
30+
run: tox

.gitignore

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Hide sublime text stuff
2+
*.sublime-project
3+
*.sublime-workspace
4+
5+
# Hide some OS X stuff
6+
.DS_Store
7+
.AppleDouble
8+
.LSOverride
9+
Icon
10+
11+
# Thumbnails
12+
._*
13+
14+
# IntelliJ IDEA
15+
.idea
16+
*.iml
17+
18+
# pytest
19+
.pytest_cache
20+
.cache
21+
22+
# GITHUB Proposed Python stuff:
23+
*.py[cod]
24+
25+
# C extensions
26+
*.so
27+
28+
# Packages
29+
*.egg
30+
*.egg-info
31+
dist
32+
build
33+
eggs
34+
.eggs
35+
parts
36+
bin
37+
var
38+
sdist
39+
develop-eggs
40+
.installed.cfg
41+
lib
42+
lib64
43+
pip-wheel-metadata
44+
45+
# Logs
46+
*.log
47+
pip-log.txt
48+
49+
# Unit test / coverage reports
50+
.coverage
51+
.tox
52+
coverage.xml
53+
nosetests.xml
54+
htmlcov/
55+
test-reports/
56+
test-results.xml
57+
test-output.xml
58+
59+
# Translations
60+
*.mo
61+
62+
# Mr Developer
63+
.mr.developer.cfg
64+
.project
65+
.pydevproject
66+
67+
.python-version
68+
69+
# emacs auto backups
70+
*~
71+
*#
72+
*.orig
73+
74+
# venv stuff
75+
pyvenv.cfg
76+
pip-selfcheck.json
77+
venv
78+
.venv
79+
Pipfile*
80+
share/*
81+
Scripts/
82+
83+
# vimmy stuff
84+
*.swp
85+
*.swo
86+
tags
87+
ctags.tmp
88+
89+
# vagrant stuff
90+
virtualization/vagrant/setup_done
91+
virtualization/vagrant/.vagrant
92+
virtualization/vagrant/config
93+
94+
# Visual Studio Code
95+
.vscode/*
96+
!.vscode/cSpell.json
97+
!.vscode/extensions.json
98+
!.vscode/tasks.json
99+
100+
# Typing
101+
.mypy_cache

__init__.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
"""Unofficial python library for the Blue Riiot Blue Connect API."""
2+
3+
4+
import asyncio
5+
import time
6+
import json
7+
import logging
8+
9+
import aiohttp
10+
from aws_request_signer import AwsRequestSigner
11+
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/"
20+
LOGGER = logging.getLogger()
21+
22+
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 = {}
39+
40+
def __init__(self, username: str, password: str, language: str = "en") -> None:
41+
"""Inititialize the api connection, a valid username and password must be provided."""
42+
self._username = username
43+
self._password = password
44+
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())
53+
54+
async def close_async(self):
55+
"""Close connection to the api."""
56+
await self._http_session.close()
57+
58+
async def fetch_data(self):
59+
"""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
103+
104+
@property
105+
def swimming_pool_device(self):
106+
"""Return Blue Connect device info for the main swimming pool."""
107+
return self._swimming_pool_device
108+
109+
@property
110+
def user_info(self):
111+
"""Return info about logged in user."""
112+
return self._user_info
113+
114+
@property
115+
def user_preferences(self):
116+
"""Return preferences of logged in user."""
117+
return self._user_info.get("userPreferences", {})
118+
119+
@property
120+
def swimming_pool_ph(self):
121+
"""Return current PH info of the main swimming pool."""
122+
return self._swimming_pool_ph
123+
124+
@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()

0 commit comments

Comments
 (0)