Skip to content

Commit cb83246

Browse files
committed
Changed library have multiple objects for multiple logins, to be more in line with Home Assistant entries. Updated tests.
1 parent a8f0895 commit cb83246

14 files changed

Lines changed: 150 additions & 295 deletions

File tree

README.md

Lines changed: 33 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ $ python setup.py install
4545
#### Async example
4646

4747
```python
48-
from crownstone_cloud import CrownstoneCloud
48+
from crownstone_cloud import CrownstoneCloud, create_clientsession
4949
import logging
5050
import asyncio
5151

@@ -54,37 +54,38 @@ logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG)
5454

5555

5656
async def main():
57+
# Every instance creates it's own websession for easy accessibility, however using 1 websession is recommended.
58+
# Create your websession like so:
59+
websession = create_clientsession()
5760
# Initialize cloud.
58-
cloud = CrownstoneCloud()
61+
cloud_user_1 = CrownstoneCloud('email_user_1', 'password_user_1', websession)
5962
# Login to the Crownstone Cloud and synchronize all cloud data.
60-
# Important is to save your user id which is returned from the function!
61-
user_id_1 = await cloud.async_initialize('email_user_1', 'password_user_1')
63+
await cloud_user_1.async_initialize()
6264

63-
# Get a crownstone by name that can dim, and put it on 20% brightness for user 1.
64-
crownstone_lamp = cloud.get_crownstone('Lamp', user_id_1)
65+
# Get a crownstone by name that can dim, and put it on 20% brightness for user 1
66+
crownstone_lamp = cloud_user_1.get_crownstone('Lamp')
6567
await crownstone_lamp.async_set_brightness(20)
6668

6769
# Login & synchronize data for an other account.
68-
user_id_2 = await cloud.async_initialize("email_user_2", "password_user_2")
70+
cloud_user_2 = CrownstoneCloud('email_user_2', 'password_user_2', websession)
71+
await cloud_user_2.async_initialize()
6972

7073
# Get a crownstone by name and turn it on for user 2.
71-
crownstone_tv = cloud.get_crownstone('TV', user_id_2)
74+
crownstone_tv = cloud_user_2.get_crownstone('TV')
7275
await crownstone_tv.async_turn_on()
7376

7477
# If you want to update specific data you can get the cloud data object for your user.
7578
# This object has all the cloud data for your user saved in it, which was synced with async_initialize()
7679
# Parts of the data can also be synced individually without touching the other data.
7780
# To sync all data at once, use async_synchronize() instead.
78-
my_cloud_data = cloud.get_cloud_data(user_id_1)
79-
# Now find the specific sphere object
80-
my_sphere = my_cloud_data.find("my_sphere_name")
81+
my_sphere = cloud_user_1.cloud_data.find("my_sphere_name")
8182
# request to sync only the locations with the cloud
8283
my_sphere.locations.async_update_location_data()
8384
# get the keys for this sphere so you can use them with the Crownstone BLE python library
8485
sphere_keys = my_sphere.async_get_keys()
8586

8687
# Close the aiohttp clientsession after we are done.
87-
await cloud.async_close_session()
88+
await websession.close()
8889

8990
asyncio.run(main())
9091
```
@@ -99,13 +100,13 @@ import logging
99100
logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG)
100101

101102
# Initialize cloud.
102-
cloud = CrownstoneCloud()
103+
cloud = CrownstoneCloud('email', 'password')
103104
# Use 'run_async' to run async functions in sync context.
104-
# Login & synchronize all cloud data. Save the user id that the function returns for later use.
105-
my_user_id = run_async(cloud.async_initialize('email', 'password'))
105+
# Login & synchronize all cloud data.
106+
run_async(cloud.async_initialize())
106107

107108
# Get a crownstone by name and turn it on.
108-
crownstone_coffee_machine = cloud.get_crownstone('Coffee machine', my_user_id)
109+
crownstone_coffee_machine = cloud.get_crownstone('Coffee machine')
109110
run_async(crownstone_coffee_machine.async_turn_on())
110111

111112
# Close the session after we are done.
@@ -114,29 +115,23 @@ run_async(cloud.async_close_session())
114115

115116
### Initialization
116117

117-
The Crownstone cloud can be created without any arguments like so:
118+
The Crownstone cloud is initialized with the email and password of a user:
118119
```python
119-
cloud = CrownstoneCloud()
120+
cloud = CrownstoneCloud('email', 'password')
120121
```
121-
A new aiohttp session will be created, which has to be closed at the end of the program.
122-
If you are using this library in existing software that already uses an own websession, you can use this like so:
123-
```python
124-
cloud = CrownstoneCloud(websession)
125-
```
126-
To log in to the Crownstone Cloud, the following are required:
127-
128-
* User email
129-
* User password
130-
131122
If you do not yet have a Crownstone account, go to [My Crownstone](https://my.crownstone.rocks) to set one up.
132123
The email and password are used to re-login after an access token has expired.
133124

125+
You can log into multiple accounts by creating more CrownstoneCloud objects. When doing so, it is recommended to use
126+
only 1 websession for all your requests. Create a websession and append it as parameter to all your CrownstoneCloud
127+
objects. Take a look at the async example above.
128+
```python
129+
cloud = CrownstoneCloud('email', 'password', websession)
130+
```
134131
To log in and get all your Crownstone from the cloud:
135132
```python
136-
await cloud.async_initialize('email', 'password')
133+
await cloud.async_initialize()
137134
```
138-
This library supports logging in to multiple accounts. Simply call `async_initialize()` again with the email and
139-
password of the other account. It is only required to call initialize once for each account.
140135

141136
## Data structure
142137

@@ -205,7 +200,7 @@ Example names of Crownstones:
205200

206201
A Crownstone has the following fields in the cloud lib:
207202
* abilities: Dict
208-
* state: Float (0..1)
203+
* state: Int (0..100)
209204
* name: String
210205
* unique_id: String
211206
* cloud_id: String
@@ -232,20 +227,17 @@ A User has the following fields in the cloud lib:
232227

233228
### Cloud
234229

235-
#### async_initialize(email: String, password: String)
230+
#### async_initialize()
236231
> Login and sync all data for the user from the cloud.
237232
238-
#### async_synchronize(user_id: String)
233+
#### async_synchronize()
239234
> Synchronize all data for a user. Use case is to update the local data with new data from the cloud.
240-
> This function is already called in `async_initialize()` for new logins.
235+
> This function is already called in `async_initialize()`.
241236
242-
#### get_cloud_data(user_id: String)
243-
> Get the cloud data object for a logged in user.
244-
245-
#### get_crownstone(crownstone_name: String, user_id: String) -> Crownstone
237+
#### get_crownstone(crownstone_name: String) -> Crownstone
246238
> Get a Crownstone object by name for a user, if it exists.
247239
248-
#### get_crownstone_by_id(crownstone_id: String, user_id: String) -> Crownstone
240+
#### get_crownstone_by_id(crownstone_id: String) -> Crownstone
249241
> Get a Crownstone object by it's id for a user, if it exists.
250242
251243
#### async_close_session()
@@ -349,8 +341,6 @@ Make sure to see the examples above!
349341

350342
## Testing
351343

352-
### Tests are not up-to-date yet for the newest commit, only run for version 1.2.1.
353-
354344
To run the tests using tox install tox first by running:
355345
```console
356346
$ pip install tox
@@ -359,7 +349,7 @@ To execute the tests cd to the project folder and run:
359349
```console
360350
$ tox
361351
```
362-
To see which parts of the code are covered by the tests, a coverage report is generated after the tests have been successfull.<br>
352+
To see which parts of the code are covered by the tests, a coverage report is generated after the tests have been successful.<br>
363353
To see the coverage report run:
364354
```console
365355
$ coverage report

crownstone_cloud/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
"""Top level imports."""
22
from crownstone_cloud.cloud import CrownstoneCloud
33
from crownstone_cloud.util.runners import run_async
4+
from crownstone_cloud.helpers.aiohttp_client import create_clientsession

crownstone_cloud/cloud.py

Lines changed: 25 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,56 +2,55 @@
22
import logging
33
import asyncio
44
import aiohttp
5-
from crownstone_cloud.cloud_models.spheres import Spheres
5+
from typing import Optional
6+
from crownstone_cloud.helpers.conversion import password_to_hash
67
from crownstone_cloud.cloud_models.crownstones import Crownstone
8+
from crownstone_cloud.cloud_models.spheres import Spheres
79
from crownstone_cloud.helpers.requests import RequestHandler
8-
from crownstone_cloud.helpers.logins import LoginManager
9-
from crownstone_cloud.const import CLOUD_DATA
1010

1111
_LOGGER = logging.getLogger(__name__)
1212

1313

1414
class CrownstoneCloud:
1515
"""Create a Crownstone cloud instance."""
1616

17-
def __init__(self, clientsession: aiohttp.ClientSession = None) -> None:
18-
# all data per session is stored here
17+
def __init__(self, email: str, password: str, clientsession: aiohttp.ClientSession = None) -> None:
18+
# Create request handler instance
1919
self.request_handler = RequestHandler(self, clientsession)
20-
self.login_manager = LoginManager(self)
20+
# Instance data
21+
self.login_data = {'email': email, 'password': password_to_hash(password)}
22+
self.access_token: Optional[str] = None
23+
self.cloud_data: Optional[Spheres] = None
2124

22-
async def async_initialize(self, email: str, password: str) -> str or None:
25+
async def async_initialize(self) -> None:
2326
"""
2427
Login to Crownstone API & synchronize all cloud data.
2528
2629
This method is a coroutine.
27-
28-
:param email: Crownstone email.
29-
:param password: Crownstone password.
30-
:return: user_id of the login.
3130
"""
32-
# login
33-
user_id = await self.login_manager.async_login(email=email, password=password)
34-
_LOGGER.debug("Login to Crownstone Cloud successful")
31+
# Login
32+
login_response = await self.request_handler.request_login(self.login_data)
3533

36-
# get data
37-
await self.async_synchronize(user_id)
34+
# Save access token & create cloud data object
35+
self.access_token = login_response['id']
36+
self.cloud_data = Spheres(self, login_response['userId'])
37+
_LOGGER.debug("Login to Crownstone Cloud successful")
3838

39-
return user_id
39+
# Synchronize data
40+
await self.async_synchronize()
4041

41-
async def async_synchronize(self, user_id: str) -> None:
42+
async def async_synchronize(self) -> None:
4243
"""
4344
Sync all data from cloud.
4445
4546
This method is a coroutine.
46-
47-
:param user_id: The user id of the login.
4847
"""
4948
_LOGGER.debug("Initiating all cloud data")
5049
# get the sphere data for this user_id
51-
await self.get_cloud_data(user_id).async_update_sphere_data()
50+
await self.cloud_data.async_update_sphere_data()
5251

5352
# get the data from the sphere attributes
54-
for sphere in self.get_cloud_data(user_id):
53+
for sphere in self.cloud_data:
5554
await asyncio.gather(
5655
sphere.async_update_sphere_presence(),
5756
sphere.crownstones.async_update_crownstone_data(),
@@ -61,27 +60,15 @@ async def async_synchronize(self, user_id: str) -> None:
6160
)
6261
_LOGGER.debug("Cloud data successfully initialized")
6362

64-
def get_cloud_data(self, user_id: str) -> Spheres:
65-
"""
66-
Get the cloud data object for a specific logged in user.
67-
68-
:param user_id: The user id of the login.
69-
:return: Spheres object. (Cloud data model)
70-
"""
71-
self.login_manager.set_context(user_id)
72-
73-
return self.login_manager.get_from_context(CLOUD_DATA)
74-
75-
def get_crownstone(self, crownstone_name, user_id: str) -> Crownstone:
63+
def get_crownstone(self, crownstone_name) -> Crownstone:
7664
"""
7765
Get a crownstone by name without specifying a sphere.
7866
7967
:param crownstone_name: Name of the Crownstone.
80-
:param user_id: The user id of the login.
8168
:return: Crownstone object.
8269
"""
8370
try:
84-
for sphere in self.get_cloud_data(user_id):
71+
for sphere in self.cloud_data:
8572
for crownstone in sphere.crownstones:
8673
if crownstone.name == crownstone_name:
8774
return crownstone
@@ -90,16 +77,15 @@ def get_crownstone(self, crownstone_name, user_id: str) -> Crownstone:
9077
except ValueError:
9178
_LOGGER.exception("No sphere data available for this login. Use 'async_synchronize' to load user data.")
9279

93-
def get_crownstone_by_id(self, crownstone_id, user_id: str) -> Crownstone:
80+
def get_crownstone_by_id(self, crownstone_id) -> Crownstone:
9481
"""
9582
Get a crownstone by id without specifying a sphere.
9683
9784
:param crownstone_id: The cloud id of the Crownstone.
98-
:param user_id: The user id of the login.
9985
:return: Crownstone object.
10086
"""
10187
try:
102-
for sphere in self.get_cloud_data(user_id):
88+
for sphere in self.cloud_data:
10389
return sphere.crownstones[crownstone_id]
10490
except KeyError:
10591
_LOGGER.exception("This login_id does not exist. Use 'async_login' to login.")

crownstone_cloud/cloud_models/crownstones.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
"""Crownstone handler for Crownstone cloud data"""
22
from crownstone_cloud.const import DIMMING_ABILITY
3+
from crownstone_cloud.exceptions import (
4+
CrownstoneAbilityError,
5+
AbilityError
6+
)
37
from typing import Dict, Any
48
import logging
59

@@ -22,7 +26,7 @@ def __iter__(self):
2226
async def async_update_crownstone_data(self) -> None:
2327
"""Get the crownstones data from the cloud."""
2428
# include abilities and current switch state in the request
25-
data_filter = {"include": ["currentSwitchState", {"abilities": "properties"}]}
29+
data_filter = {"include": ["currentSwitchStateV2", {"abilities": "properties"}]}
2630
# request data
2731
crownstone_data = await self.cloud.request_handler.get(
2832
'Spheres', 'ownedStones', filter=data_filter, model_id=self.sphere_id
@@ -107,7 +111,8 @@ def __init__(self, cloud, data: Dict[str, Any]) -> None:
107111
self.cloud = cloud
108112
self.data: Dict[str, Any] = data
109113
self.abilities: Dict[str, CrownstoneAbility] = {}
110-
self._context: str = self.cloud.login_manager.get_context()
114+
# power usage (W)
115+
self.power_usage = None
111116

112117
@property
113118
def name(self) -> str:
@@ -164,8 +169,6 @@ async def async_turn_on(self) -> None:
164169
165170
This method is a coroutine.
166171
"""
167-
# make sure to use the context of what the object was created in.
168-
self.cloud.login_manager.set_context(self._context)
169172
# send a command to the cloud to turn the Crownstone on.
170173
await self.cloud.request_handler.post(
171174
'Stones', 'switch', model_id=self.cloud_id, json={"type": "TURN_ON"}
@@ -177,8 +180,6 @@ async def async_turn_off(self) -> None:
177180
178181
This method is a coroutine.
179182
"""
180-
# make sure to use the context of what the object was created in.
181-
self.cloud.login_manager.set_context(self._context)
182183
# send a command to the cloud to turn the Crownstone off.
183184
await self.cloud.request_handler.post(
184185
'Stones', 'switch', model_id=self.cloud_id, json={"type": "TURN_OFF"}
@@ -192,16 +193,13 @@ async def async_set_brightness(self, brightness: int) -> None:
192193
193194
This method is a coroutine.
194195
"""
195-
# make sure to use the context of what the object was created in.
196-
self.cloud.login_manager.set_context(self._context)
197196
# check dimming availability & value, and send a command to the cloud to dim the Crownstone.
198197
if self.abilities[DIMMING_ABILITY].is_enabled:
199198
if brightness < 0 or brightness > 100:
200199
raise ValueError("Enter a value between 0 and 100")
201200
else:
202201
await self.cloud.request_handler.post(
203-
'Stones', 'switch', model_id=self.cloud_id, json={"type": "PERCENTAGE","percentage": brightness}
202+
'Stones', 'switch', model_id=self.cloud_id, json={"type": "PERCENTAGE", "percentage": brightness}
204203
)
205204
else:
206-
_LOGGER.warning("Dimming is not enabled for this crownstone. Go to the crownstone app to enable it")
207-
# TODO: raise error, or just try to set brightness anyway?
205+
raise CrownstoneAbilityError(AbilityError["NOT_ENABLED"])

crownstone_cloud/cloud_models/spheres.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ def __init__(self, cloud, data: Dict[str, Any], user_id: str) -> None:
7575
self.cloud = cloud
7676
self.data: Dict[str, Any] = data
7777
self.user_id: str = user_id
78-
self._context = self.cloud.login_manager.get_context()
7978
self.crownstones = Crownstones(self.cloud, self.cloud_id)
8079
self.locations = Locations(self.cloud, self.cloud_id)
8180
self.users = Users(self.cloud, self.cloud_id)
@@ -103,8 +102,6 @@ async def async_get_keys(self) -> dict:
103102
104103
This method is a coroutine.
105104
"""
106-
# make sure to use the context of what the object was created in.
107-
self.cloud.login_manager.set_context(self._context)
108105
# get & reformat keys.
109106
keys = await self.cloud.request_handler.get('users', 'keysV2', model_id=self.user_id)
110107
for key_set in keys:
@@ -120,8 +117,6 @@ async def async_update_sphere_presence(self) -> None:
120117
121118
This method is a coroutine.
122119
"""
123-
# make sure to use the context of what the object was created in.
124-
self.cloud.login_manager.set_context(self._context)
125120
# get presence and create a list with user id's who are in the sphere.
126121
self.present_people = []
127122
presence_data = await self.cloud.request_handler.get('Spheres', 'presentPeople', model_id=self.cloud_id)

0 commit comments

Comments
 (0)