Skip to content

Commit 58eae05

Browse files
authored
Merge pull request #3 from rado0x54/develop
Release 0.1.2
2 parents 9ff344a + 950405d commit 58eae05

5 files changed

Lines changed: 72 additions & 30 deletions

File tree

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ pip install pysnoo
1717

1818
## Programmatic Usage
1919
Programatically, the project provides two main class inferfaces. The Snoo API Client interface
20-
[snoo.py](./pysnoo/snoo.py) and the Snoo PubNub interface [pubnub.py](./pysnoo/pubnub.py).
20+
[snoo.py](https://github.com/rado0x54/pysnoo/blob/master/pysnoo/snoo.py) and the Snoo PubNub
21+
interface [pubnub.py](https://github.com/rado0x54/pysnoo/blob/master/pysnoo/pubnub.py).
2122

2223
Here's a short example to setup both. It uses the Snoo API Interface to get the Snoo serial number,
2324
and access token, which are required to initialize the PubNub interface. More usage examples can be
24-
found by looking at the [CLI Tool](./scripts/snoo) or the [unit tests](./tests).
25+
found by looking at the [CLI Tool](https://github.com/rado0x54/pysnoo/blob/master/scripts/snoo) or
26+
the [unit tests](https://github.com/rado0x54/pysnoo/tree/master/tests).
2527

2628
```python
2729
async with SnooAuthSession(token, token_updater) as auth:
@@ -39,10 +41,9 @@ async with SnooAuthSession(token, token_updater) as auth:
3941
print('There is no Snoo connected to that account!')
4042
else:
4143
# Snoo PubNub Interface
42-
pubnub = SnooPubNub(snoo.auth.access_token,
43-
devices[0].serial_number,
44-
f'pn-pysnoo-{devices[0].serial_number}',
45-
callback)
44+
pubnub = SnooPubNub(auth.access_token,
45+
devices[0].serial_number,
46+
f'pn-pysnoo-{devices[0].serial_number}')
4647

4748
last_activity_state = (await pubnub.history())[0]
4849
if last_activity_state.state_machine.state == SessionLevel.ONLINE:

pysnoo/pubnub.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from .models import ActivityState, SessionLevel
1111
from .const import SNOO_PUBNUB_PUBLISH_KEY, SNOO_PUBNUB_SUBSCRIBE_KEY
1212

13+
_LOGGER = logging.getLogger(__name__)
14+
1315

1416
class SnooSubscribeListener(SubscribeCallback):
1517
"""Snoo Subscription Listener Class"""
@@ -24,10 +26,12 @@ def status(self, pubnub, status):
2426
"""PubNub Status Callback Implementation"""
2527
if utils.is_subscribed_event(status) and not self.connected_event.is_set():
2628
self.connected_event.set()
29+
self.disconnected_event.clear()
2730
elif utils.is_unsubscribed_event(status) and not self.disconnected_event.is_set():
2831
self.disconnected_event.set()
32+
self.connected_event.clear()
2933
elif status.is_error():
30-
logging.error('Error in Snoo PubNub Listener of Category: %s', status.category)
34+
_LOGGER.error('Error in Snoo PubNub Listener of Category: %s', status.category)
3135

3236
def message(self, pubnub, message):
3337
"""PubNub Message Callback Implementation"""
@@ -36,6 +40,10 @@ def message(self, pubnub, message):
3640
def presence(self, pubnub, presence):
3741
"""PubNub Presence Callback Implementation"""
3842

43+
def is_connected(self):
44+
"""Returns true if the listener is currently connected to an active subscription"""
45+
return self.connected_event.is_set()
46+
3947
async def wait_for_connect(self):
4048
"""Async utility function that waits for subscription connect."""
4149
if not self.connected_event.is_set():
@@ -63,6 +71,8 @@ def __init__(self,
6371
self._controlcommand_channel = 'ControlCommand.{}'.format(serial_number)
6472
self._pubnub = PubNubAsyncio(self.config, custom_event_loop=custom_event_loop)
6573
self._listener = SnooSubscribeListener(self._activy_state_callback)
74+
# Add listener
75+
self._pubnub.add_listener(self._listener)
6676
self._external_listeners: List[Callable[[ActivityState], None]] = []
6777

6878
@staticmethod
@@ -95,20 +105,35 @@ def _activy_state_callback(self, state: ActivityState):
95105
for update_callback in self._external_listeners:
96106
update_callback(state)
97107

98-
async def subscribe(self):
108+
def subscribe(self):
99109
"""Subscribe to Snoo Activity Channel"""
100-
self._pubnub.add_listener(self._listener)
110+
if self._listener.is_connected():
111+
_LOGGER.warning('Trying to subscribe PubNub instance that is already subscribed to %s',
112+
self._activiy_channel)
113+
return
114+
101115
self._pubnub.subscribe().channels([
102116
self._activiy_channel
103117
]).execute()
104118

119+
async def subscribe_and_await_connect(self):
120+
"""Subscribe to Snoo Activity Channel and await connect"""
121+
self.subscribe()
105122
await self._listener.wait_for_connect()
106123

107-
async def unsubscribe(self):
124+
def unsubscribe(self):
108125
"""Unsubscribe to Snoo Activity Channel"""
126+
if not self._listener.is_connected():
127+
_LOGGER.warning('Trying to unsubscribe PubNub instance that is NOT subscribed to %s', self._activiy_channel)
128+
return
129+
109130
self._pubnub.unsubscribe().channels(
110131
self._activiy_channel
111132
).execute()
133+
134+
async def unsubscribe_and_await_disconnect(self):
135+
"""Unsubscribe to Snoo Activity Channel and await disconnect"""
136+
self.unsubscribe()
112137
await self._listener.wait_for_disconnect()
113138

114139
async def history(self, count=1):

scripts/snoo

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,15 +89,15 @@ async def monitor(snoo: Snoo, args):
8989
for activity_state in await pubnub.history():
9090
as_callback(activity_state)
9191

92-
await pubnub.subscribe()
92+
await pubnub.subscribe_and_await_connect()
9393

9494
try:
9595
while True:
9696
await asyncio.sleep(1)
9797
except asyncio.CancelledError:
9898
pass
9999
finally:
100-
await pubnub.unsubscribe()
100+
await pubnub.unsubscribe_and_await_disconnect()
101101
await pubnub.stop()
102102

103103

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""PySnoo setup script."""
22
from setuptools import setup
33

4-
_VERSION = '0.1.1'
4+
_VERSION = '0.1.2'
55

66

77
def readme():

tests/test_snoo_pubnub.py

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import json
33

44
from pubnub.enums import PNOperationType, PNStatusCategory
5-
from pubnub.callbacks import SubscribeCallback
65
from pubnub.models.consumer.common import PNStatus
76
from pubnub.models.consumer.pubsub import PNMessageResult
87

@@ -90,33 +89,39 @@ async def test_publish_goto_state_with_hold(self, mocked_request):
9089
self.assertEqual(options.query_string,
9190
f'auth=ACCESS_TOKEN&pnsdk=PubNub-Python-Asyncio%2F{self.pubnub._pubnub.SDK_VERSION}&uuid=UUID')
9291

93-
@patch('pubnub.pubnub_core.PubNubCore.add_listener')
9492
@patch('pubnub.managers.SubscriptionManager.adapt_subscribe_builder')
95-
async def test_subscribe(self, mocked_subscribe_builder, mocked_add_listener):
93+
async def test_subscribe_and_await_connect(self, mocked_subscribe_builder):
9694
"""Test subscribe"""
97-
# Setup
98-
99-
def add_listener_side_effect(listener: SubscribeCallback):
100-
# Call Connect Status.
101-
pn_status = PNStatus()
102-
pn_status.category = PNStatusCategory.PNConnectedCategory
103-
# Call after 1s: listener.status(self.pubnub._pubnub, pn_status)
104-
self.loop.call_later(1, listener.status, self.pubnub._pubnub, pn_status) # pylint: disable=protected-access
105-
106-
mocked_add_listener.side_effect = add_listener_side_effect
95+
# pylint: disable=protected-access
96+
# Call Connect Status.
97+
pn_status = PNStatus()
98+
pn_status.category = PNStatusCategory.PNConnectedCategory
99+
# Call after 1s: listener.status(self.pubnub._pubnub, pn_status)
100+
self.loop.call_later(1, self.pubnub._listener.status,
101+
self.pubnub._pubnub, pn_status)
107102

108-
await self.pubnub.subscribe()
103+
await self.pubnub.subscribe_and_await_connect()
109104

110-
mocked_add_listener.assert_called_once()
111105
mocked_subscribe_builder.assert_called_once()
112106
subscribe_operation = mocked_subscribe_builder.mock_calls[0][1][0]
113107
self.assertEqual(subscribe_operation.channels, ['ActivityState.SERIAL_NUMBER'])
114108
self.assertEqual(subscribe_operation.channel_groups, [])
115109
self.assertEqual(subscribe_operation.presence_enabled, False)
116110
self.assertEqual(subscribe_operation.timetoken, 0)
117111

112+
@patch('pubnub.managers.SubscriptionManager.adapt_subscribe_builder')
113+
def test_prevent_multiple_subscription(self, mocked_subscribe_builder):
114+
"""Test prevent multiple subscriptions"""
115+
# pylint: disable=protected-access
116+
# Set Listener as connected
117+
self.pubnub._listener.connected_event.set()
118+
119+
self.pubnub.subscribe()
120+
121+
mocked_subscribe_builder.assert_not_called()
122+
118123
@patch('pubnub.managers.SubscriptionManager.adapt_unsubscribe_builder')
119-
async def test_unsubscribe(self, mocked_unsubscribe_builder):
124+
async def test_unsubscribe_and_await_disconnect(self, mocked_unsubscribe_builder):
120125
"""Test unsubscribe"""
121126
# pylint: disable=protected-access
122127
# Call Connect Status.
@@ -125,14 +130,25 @@ async def test_unsubscribe(self, mocked_unsubscribe_builder):
125130
pn_status.operation = PNOperationType.PNUnsubscribeOperation
126131
# Call after 1s: listener.status(self.pubnub._pubnub, pn_status)
127132
self.loop.call_later(1, self.pubnub._listener.status, self.pubnub._pubnub, pn_status)
133+
# Listener is connected:
134+
self.pubnub._listener.connected_event.set()
128135

129-
await self.pubnub.unsubscribe()
136+
await self.pubnub.unsubscribe_and_await_disconnect()
130137

131138
mocked_unsubscribe_builder.assert_called_once()
132139
unsubscribe_operation = mocked_unsubscribe_builder.mock_calls[0][1][0]
133140
self.assertEqual(unsubscribe_operation.channels, ['ActivityState.SERIAL_NUMBER'])
134141
self.assertEqual(unsubscribe_operation.channel_groups, [])
135142

143+
@patch('pubnub.managers.SubscriptionManager.adapt_unsubscribe_builder')
144+
def test_prevent_multiple_unsubscription(self, mocked_unsubscribe_builder):
145+
"""Test prevent multiple unsubscriptions"""
146+
147+
# Listener is disconnected (initial state)
148+
self.pubnub.unsubscribe()
149+
150+
mocked_unsubscribe_builder.assert_not_called()
151+
136152
@patch('pubnub.pubnub_asyncio.PubNubAsyncio.request_future')
137153
async def test_history(self, mocked_request):
138154
"""Test history"""

0 commit comments

Comments
 (0)