Skip to content

Commit 4783b4c

Browse files
authored
Merge pull request #2022 from Drakkar-Software/dev
Update master
2 parents dcbd3a1 + 49997c6 commit 4783b4c

File tree

12 files changed

+111
-99
lines changed

12 files changed

+111
-99
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ __pycache__/
99

1010
# Distribution / packaging
1111
.Python
12-
.idea
1312
.pytest_cache/
1413
env/
1514
build/
@@ -102,6 +101,8 @@ ENV/
102101

103102
# IDE
104103
.vscode/
104+
.idea
105+
.gitpod.yml
105106

106107
# Tentacles manager temporary files
107108
octobot/creator_temp/

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
*It is strongly advised to perform an update of your tentacles after updating OctoBot. (start.py tentacles --install --all)*
88

9+
## [0.4.8] - 2022-09-04
10+
### Fixed
11+
- Device creation
12+
13+
## [0.4.7] - 2022-09-03
14+
### Updated
15+
- [Astrolab] Improvements and fixes
16+
917
## [0.4.6] - 2022-08-23
1018
### Added
1119
- [Trading] Futures trading

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# OctoBot [0.4.6](https://octobot.click/gh-changelog)
1+
# OctoBot [0.4.8](https://octobot.click/gh-changelog)
22
[![PyPI](https://img.shields.io/pypi/v/OctoBot.svg)](https://octobot.click/gh-pypi)
33
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/e07fb190156d4efb8e7d07aaa5eff2e1)](https://app.codacy.com/gh/Drakkar-Software/OctoBot?utm_source=github.com&utm_medium=referral&utm_content=Drakkar-Software/OctoBot&utm_campaign=Badge_Grade_Dashboard)[![Downloads](https://pepy.tech/badge/octobot/month)](https://pepy.tech/project/octobot)
44
[![Dockerhub](https://img.shields.io/docker/pulls/drakkarsoftware/octobot.svg)](https://octobot.click/gh-dockerhub)

octobot/community/authentication.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import base64
1818
import contextlib
1919
import json
20+
import time
21+
2022
import requests
2123
import aiohttp
2224

@@ -55,7 +57,7 @@ def __init__(self, authentication_url, feed_url, config=None):
5557
self._aiohttp_session = None
5658
self._cache = {}
5759
self._fetch_account_task = None
58-
self._fetch_device_uuid_task = None
60+
self._restart_task = None
5961
self._community_feed = None
6062
self._login_completed = None
6163

@@ -100,7 +102,7 @@ def _create_community_feed_if_necessary(self) -> bool:
100102

101103
async def _ensure_init_community_feed(self):
102104
self._create_community_feed_if_necessary()
103-
if not self._community_feed.is_connected():
105+
if not self._community_feed.is_connected() and self._community_feed.can_connect():
104106
if self.initialized_event is not None and not self.initialized_event.is_set():
105107
await asyncio.wait_for(self.initialized_event.wait(), self.LOGIN_TIMEOUT)
106108
if not self.is_logged_in():
@@ -134,10 +136,13 @@ async def async_graphql_query(self, query, query_name, variables=None, operation
134136
expected_code=None, allow_retry_on_expired_token=True):
135137
try:
136138
async with self._authenticated_qgl_session() as session:
139+
t0 = time.time()
140+
self.logger.debug(f"starting {query_name} graphql query")
137141
resp = await session.post(
138142
f"{identifiers_provider.IdentifiersProvider.GQL_BACKEND_API_URL}",
139143
json=self._build_gql_request_body(query, variables, operation_name)
140144
)
145+
self.logger.debug(f"graphql query {query_name} done in {time.time() - t0} seconds")
141146
if resp.status == 401:
142147
# access token expired
143148
raise authentication.AuthenticationRequired
@@ -254,7 +259,7 @@ async def on_new_device_select(self):
254259
await self._update_feed_device_uuid()
255260

256261
async def _fetch_devices(self):
257-
query, variables = graphql_requests.select_devices(self.user_account.gql_user_id)
262+
query, variables = graphql_requests.select_devices()
258263
return await self.async_graphql_query(query, "devices", variables=variables, expected_code=200)
259264

260265
async def fetch_device(self, device_id):
@@ -263,21 +268,20 @@ async def fetch_device(self, device_id):
263268

264269
async def create_new_device(self):
265270
await self.gql_login_if_required()
266-
query, variables = graphql_requests.create_new_device_query(self.user_account.gql_user_id)
267-
return await self.async_graphql_query(query, "insertOneDevice", variables=variables, expected_code=200)
271+
query, variables = graphql_requests.create_new_device_query()
272+
return await self.async_graphql_query(query, "createDevice", variables=variables, expected_code=200)
268273

269274
async def _update_feed_device_uuid(self):
270275
self._create_community_feed_if_necessary()
271276
if self._community_feed.associated_gql_device_id != self.user_account.gql_device_id:
272-
# only device id changed, need to refresh uuid. Otherwise it means that no feed was started with a
277+
# only device id changed, need to refresh uuid. Otherwise, it means that no feed was started with a
273278
# different uuid, no need to update
274-
# reset _fetch_device_uuid_task if running
275-
if self._fetch_device_uuid_task is not None and not self._fetch_device_uuid_task.done():
276-
self._fetch_device_uuid_task.cancel()
279+
# reset restart task if running
280+
if self._restart_task is not None and not self._restart_task.done():
281+
self._restart_task.cancel()
277282
self._community_feed.remove_device_details()
278-
task = self._community_feed.restart if self._community_feed.is_connected() \
279-
else self._community_feed.fetch_mqtt_device_uuid
280-
self._fetch_device_uuid_task = asyncio.create_task(task())
283+
if self._community_feed.is_connected() or not self._community_feed.can_connect():
284+
self._restart_task = asyncio.create_task(self._community_feed.restart())
281285

282286
def logout(self):
283287
"""
@@ -287,10 +291,10 @@ def logout(self):
287291
self._reset_tokens()
288292
self.clear_cache()
289293
self.remove_login_detail()
290-
for task in (self._fetch_device_uuid_task, self._fetch_account_task):
294+
for task in (self._restart_task, self._fetch_account_task):
291295
if task is not None and not task.done():
292296
task.cancel()
293-
self._fetch_device_uuid_task = self._fetch_account_task = None
297+
self._restart_task = self._fetch_account_task = None
294298
self._create_community_feed_if_necessary()
295299
self._community_feed.remove_device_details()
296300

@@ -349,8 +353,8 @@ def get_aiohttp_session(self):
349353
async def stop(self):
350354
if self.is_initialized():
351355
self._fetch_account_task.cancel()
352-
if self._fetch_device_uuid_task is not None and not self._fetch_device_uuid_task.done():
353-
self._fetch_device_uuid_task.cancel()
356+
if self._restart_task is not None and not self._restart_task.done():
357+
self._restart_task.cancel()
354358
if self._aiohttp_session is not None:
355359
await self._aiohttp_session.close()
356360

octobot/community/errors.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#
1414
# You should have received a copy of the GNU General Public
1515
# License along with OctoBot. If not, see <https://www.gnu.org/licenses/>.
16+
import octobot_commons.authentication as commons_authentication
1617

1718

1819
class RequestError(Exception):
@@ -21,3 +22,7 @@ class RequestError(Exception):
2122

2223
class StatusCodeRequestError(RequestError):
2324
pass
25+
26+
27+
class DeviceError(commons_authentication.UnavailableError):
28+
pass

octobot/community/feeds/abstract_feed.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,6 @@ async def register_feed_callback(self, channel_type, callback, identifier=None):
3838

3939
async def send(self, message, channel_type, identifier, **kwargs):
4040
raise NotImplementedError("send is not implemented")
41+
42+
def can_connect(self):
43+
return True

octobot/community/feeds/community_mqtt_feed.py

Lines changed: 44 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -55,33 +55,42 @@ def __init__(self, feed_url, authenticator):
5555
self.associated_gql_device_id = None
5656

5757
self._mqtt_client: gmqtt.Client = None
58+
self._valid_auth = True
5859
self._device_uuid: str = None
59-
self._fetching_uuid = False
6060
self._subscription_attempts = 0
61-
self._fetched_uuid = asyncio.Event()
6261
self._subscription_topics = set()
6362
self._reconnect_task = None
63+
self._connect_task = None
64+
self._connected_at_least_once = False
6465
self._processed_messages = set()
6566

6667
async def start(self):
6768
self.should_stop = False
68-
await self.fetch_mqtt_device_uuid()
69+
self._device_uuid = self.authenticator.user_account.get_selected_device_uuid()
6970
await self._connect()
7071

7172
async def stop(self):
7273
self.should_stop = True
7374
await self._stop_mqtt_client()
7475
if self._reconnect_task is not None and not self._reconnect_task.done():
7576
self._reconnect_task.cancel()
77+
if self._connect_task is not None and not self._connect_task.done():
78+
self._connect_task.cancel()
7679
self._reset()
7780

7881
async def restart(self):
79-
await self.stop()
80-
await self.start()
82+
try:
83+
await self.stop()
84+
await self.start()
85+
except Exception as e:
86+
self.logger.exception(e, True, f"Error when restarting mqtt feed: {e}")
8187

8288
def _reset(self):
89+
self._connected_at_least_once = False
8390
self._subscription_attempts = 0
8491
self._subscription_topics = set()
92+
self._connect_task = None
93+
self._valid_auth = True
8594

8695
async def _stop_mqtt_client(self):
8796
if self.is_connected():
@@ -90,6 +99,9 @@ async def _stop_mqtt_client(self):
9099
def is_connected(self):
91100
return self._mqtt_client is not None and self._mqtt_client.is_connected
92101

102+
def can_connect(self):
103+
return self._valid_auth
104+
93105
async def register_feed_callback(self, channel_type, callback, identifier=None):
94106
topic = self._build_topic(channel_type, identifier)
95107
try:
@@ -98,57 +110,14 @@ async def register_feed_callback(self, channel_type, callback, identifier=None):
98110
self.feed_callbacks[topic] = [callback]
99111
if topic not in self._subscription_topics:
100112
self._subscription_topics.add(topic)
101-
self._subscribe((topic, ))
102-
103-
async def fetch_mqtt_device_uuid(self):
104-
if self._fetching_uuid:
105-
self.logger.info(f"Waiting for feed UUID fetching")
106-
await asyncio.wait_for(self._fetched_uuid.wait(), self.DEVICE_CREATE_TIMEOUT + 2)
107-
else:
108-
await self._fetch_mqtt_device_uuid()
113+
if self._valid_auth:
114+
self._subscribe((topic, ))
115+
else:
116+
self.logger.error(f"Can't subscribe to {channel_type.name} feed, invalid authentication")
109117

110118
def remove_device_details(self):
111-
self._fetched_uuid.clear()
112119
self._device_uuid = None
113120

114-
async def _fetch_mqtt_device_uuid(self):
115-
try:
116-
self._fetching_uuid = True
117-
if device_uuid := self.authenticator.user_account.get_selected_device_uuid():
118-
self._device_uuid = device_uuid
119-
self.logger.debug("Using fetched mqtt device id")
120-
else:
121-
await self._poll_mqtt_device_uuid()
122-
self.logger.debug("Successfully waited for mqtt device id")
123-
except Exception as e:
124-
self.logger.exception(e, True, f"Error when fetching device id: {e}")
125-
raise
126-
finally:
127-
self._fetching_uuid = False
128-
self._fetched_uuid.set()
129-
130-
async def _poll_mqtt_device_uuid(self):
131-
t0 = time.time()
132-
while time.time() - t0 < self.DEVICE_CREATE_TIMEOUT:
133-
# loop until the device uuid is available
134-
# used when a new gql device is created its uuid is not instantly filled
135-
try:
136-
device_data = await self.authenticator.fetch_device(self.authenticator.user_account.gql_device_id)
137-
if device_data is None:
138-
raise errors.RequestError(f"Error when fetching mqtt device uuid: can't find content with id: "
139-
f"{self.authenticator.user_account.gql_device_id}")
140-
elif device_uuid := device_data["uuid"]:
141-
self._device_uuid = device_uuid
142-
return
143-
# retry soon
144-
await asyncio.sleep(self.DEVICE_CREATION_REFRESH_DELAY)
145-
# should never happen unless there is a real issue
146-
except errors.RequestError:
147-
raise
148-
raise errors.RequestError(
149-
f"Timeout when fetching mqtt device uuid: no uuid to be found after {self.DEVICE_CREATE_TIMEOUT} seconds"
150-
)
151-
152121
@staticmethod
153122
def _build_topic(channel_type, identifier):
154123
return f"{channel_type.value}/{identifier}"
@@ -180,6 +149,9 @@ def _should_process(self, parsed_message):
180149
return True
181150

182151
async def send(self, message, channel_type, identifier, **kwargs):
152+
if not self._valid_auth:
153+
self.logger.warning(f"Can't send {channel_type.name}, invalid feed authentication.")
154+
return
183155
topic = self._build_topic(channel_type, identifier)
184156
self.logger.debug(f"Sending message on topic: {topic}, message: {message}")
185157
self._mqtt_client.publish(
@@ -241,8 +213,12 @@ async def _reconnect(self, client):
241213
await asyncio.sleep(delay)
242214

243215
def _on_disconnect(self, client, packet, exc=None):
244-
self.logger.info(f"Disconnected, client_id: {client._client_id}")
245-
self._try_reconnect_if_necessary(client)
216+
if self._connected_at_least_once:
217+
self.logger.info(f"Disconnected, client_id: {client._client_id}")
218+
self._try_reconnect_if_necessary(client)
219+
else:
220+
if self._connect_task is not None and not self._connect_task.done():
221+
self._connect_task.cancel()
246222

247223
def _on_subscribe(self, client, mid, qos, properties):
248224
# from https://github.com/wialon/gmqtt/blob/master/examples/resubscription.py#L28
@@ -282,12 +258,25 @@ def _update_client_config(self, client):
282258
client.set_config(default_config)
283259

284260
async def _connect(self):
261+
if self._device_uuid is None:
262+
self._valid_auth = False
263+
raise errors.DeviceError("mqtt device uuid is None, impossible to connect client")
285264
self._mqtt_client = gmqtt.Client(self.__class__.__name__)
286265
self._update_client_config(self._mqtt_client)
287266
self._register_callbacks(self._mqtt_client)
288267
self._mqtt_client.set_auth_credentials(self._device_uuid, None)
289268
self.logger.debug(f"Connecting client")
290-
await self._mqtt_client.connect(self.feed_url, self.mqtt_broker_port, version=self.MQTT_VERSION)
269+
self._connect_task = asyncio.create_task(
270+
self._mqtt_client.connect(self.feed_url, self.mqtt_broker_port, version=self.MQTT_VERSION)
271+
)
272+
try:
273+
await self._connect_task
274+
self._connected_at_least_once = True
275+
except asyncio.CancelledError:
276+
# got cancelled by on_disconnect, can't connect
277+
self.logger.error(f"Can't connect to server, please check your device uuid. "
278+
f"Current mqtt uuid is: {self._device_uuid}")
279+
self._valid_auth = False
291280

292281
def _subscribe(self, topics):
293282
if not topics:

octobot/community/graphql_requests.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,17 @@
1515
# License along with OctoBot. If not, see <https://www.gnu.org/licenses/>.
1616

1717

18-
def create_new_device_query(user_id) -> (str, dict):
18+
def create_new_device_query() -> (str, dict):
1919
return """
20-
mutation CreateDevice($user_id: ObjectId) {
21-
insertOneDevice(data: {user_id: $user_id}) {
20+
mutation CreateDevice {
21+
createDevice {
2222
_id
23+
name
24+
user_id
25+
uuid
2326
}
2427
}
25-
""", {"user_id": user_id}
28+
""", {}
2629

2730

2831
def select_device(device_id) -> (str, dict):
@@ -37,13 +40,13 @@ def select_device(device_id) -> (str, dict):
3740
""", {"_id": device_id}
3841

3942

40-
def select_devices(user_id) -> (str, dict):
43+
def select_devices() -> (str, dict):
4144
return """
42-
query SelectDevices($user_id: ObjectId) {
43-
devices(query: {user_id: $user_id}) {
45+
query SelectDevices {
46+
devices {
4447
_id
4548
uuid
4649
name
4750
}
4851
}
49-
""", {"user_id": user_id}
52+
""", {}

0 commit comments

Comments
 (0)