Skip to content

Commit

Permalink
Add support for Async operations on Worker (#38)
Browse files Browse the repository at this point in the history
* Add support for Async operations on Worker
  • Loading branch information
ocalvo committed Jun 14, 2020
1 parent 08065e9 commit 13927d8
Show file tree
Hide file tree
Showing 5 changed files with 353 additions and 0 deletions.
1 change: 1 addition & 0 deletions NEWS.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
3.0
===

* Add support for asyncio in the gammu worker
* Dropped support for Python 2.
* Windows binaries built with Gammu 1.41.0.

Expand Down
117 changes: 117 additions & 0 deletions examples/async.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# vim: expandtab sw=4 ts=4 sts=4:
#
# Copyright © 2003 - 2018 Michal Čihař <michal@cihar.com>
#
# This file is part of python-gammu <https://wammu.eu/python-gammu/>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
'''
python-gammu - Phone communication libary
Gammu asynchronous wrapper example with asyncio. This allows your application to care
only about handling received data and not about phone communication
details.
'''

import sys
import gammu
import gammu.asyncworker
import asyncio


async def send_message_async(state_machine, number, message):
smsinfo = {
'Class': -1,
'Unicode': False,
'Entries': [
{
'ID': 'ConcatenatedTextLong',
'Buffer': message
}
]}
# Encode messages
encoded = gammu.EncodeSMS(smsinfo)
# Send messages
for message in encoded:
# Fill in numbers
message['SMSC'] = {'Location': 1}
message['Number'] = number
# Actually send the message
await state_machine.send_sms_async(message)

async def get_network_info(worker):
info = await worker.get_network_info_async()
print('NetworkName:',info['NetworkName'])
print(' State:',info['State'])
print(' NetworkCode:',info['NetworkCode'])
print(' CID:',info['CID'])
print(' LAC:',info['LAC'])

async def get_info(worker):
print('Phone infomation:')
manufacturer = await worker.get_manufacturer_async()
print(('{0:<15}: {1}'.format('Manufacturer', manufacturer)))
model = await worker.get_model_async()
print(('{0:<15}: {1} ({2})'.format('Model', model[0], model[1])))
imei = await worker.get_imei_async()
print(('{0:<15}: {1}'.format('IMEI', imei)))
firmware = await worker.get_firmware_async()
print(('{0:<15}: {1}'.format('Firmware', firmware[0])))

async def main():

gammu.SetDebugFile(sys.stderr)
gammu.SetDebugLevel('textall')

config = dict(Device="/dev/ttyS6", Connection="at")
worker = gammu.asyncworker.GammuAsyncWorker()
worker.configure(config)

try:
await worker.init_async()

await get_info(worker)
await get_network_info(worker)

await send_message_async(worker, '6700', 'BAL')

# Just a busy waiting for event
# We need to keep communication with phone to get notifications
print('Press Ctrl+C to interrupt')
while 1:
try:
signal = await worker.get_signal_quality_async()
print('Signal is at {0:d}%'.format(signal['SignalPercent']))
except Exception as e:
print('Exception reading signal: {0}'.format(e))

await asyncio.sleep(10);

except Exception as e:
print('Exception:')
print(e)

print("Terminate Start")
await worker.terminate_async()
print("Terminate Done")

if __name__ == '__main__':
event_loop = asyncio.get_event_loop()
try:
event_loop.run_until_complete(main())
finally:
event_loop.close()

1 change: 1 addition & 0 deletions gammu/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
__all__ = [
'data',
'worker',
'asyncworker',
'smsd',
'exception',
]
145 changes: 145 additions & 0 deletions gammu/asyncworker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
"""Async extensions for gammu."""
import asyncio
import gammu
import gammu.worker

class GammuAsyncThread(gammu.worker.GammuThread):
"""Thread for phone communication."""

def __init__(self, queue, config, callback):
"""Initialize thread."""
super().__init__(queue, config, callback)

def _do_command(self, future, cmd, params, percentage=100):
"""Execute single command on phone."""
func = getattr(self._sm, cmd)
result = None
try:
if params is None:
result = func()
elif isinstance(params, dict):
result = func(**params)
else:
result = func(*params)
except gammu.GSMError as info:
errcode = info.args[0]["Code"]
error = gammu.ErrorNumbers[errcode]
self._callback(future, result, error, percentage)
except Exception as exception: # pylint: disable=broad-except
self._callback(future, None, exception, percentage)
else:
self._callback(future, result, None, percentage)


class GammuAsyncWorker(gammu.worker.GammuWorker):
"""Extend gammu worker class for async operations."""

def worker_callback(self, name, result, error, percents):
"""Execute command from the thread worker."""
future = None
if name == "Init" and self._init_future is not None:
future = self._init_future
elif name == "Terminate" and self._terminate_future is not None:
# Set _kill to true on the base class to avoid waiting for termination
self._thread._kill = True # pylint: disable=protected-access
future = self._terminate_future
elif hasattr(name, "set_result"):
future = name

if future is not None:
if error is None:
self._loop.call_soon_threadsafe(future.set_result, result)
else:
exception = error
if not isinstance(error, Exception):
exception = gammu.GSMError(error)
self._loop.call_soon_threadsafe(future.set_exception, exception)

def __init__(self):
"""Initialize the worker class.
@param callback: See L{GammuThread.__init__} for description.
"""
super().__init__(self.worker_callback)
self._loop = asyncio.get_event_loop()
self._init_future = None
self._terminate_future = None
self._thread = None

async def init_async(self):
"""Connect to phone."""
self._init_future = self._loop.create_future()

self._thread = GammuAsyncThread(self._queue, self._config, self._callback)
self._thread.start()

await self._init_future
self._init_future = None

async def get_imei_async(self):
"""Get the IMEI of the device."""
future = self._loop.create_future()
self.enqueue(future, commands=[("GetIMEI", ())])
return await future

async def get_network_info_async(self):
"""Get the network info in the device."""
future = self._loop.create_future()
self.enqueue(future, commands=[("GetNetworkInfo", ())])
return await future

async def get_manufacturer_async(self):
"""Get the manufacturer of the device."""
future = self._loop.create_future()
self.enqueue(future, commands=[("GetManufacturer", ())])
return await future

async def get_model_async(self):
"""Get the model of the device."""
future = self._loop.create_future()
self.enqueue(future, commands=[("GetModel", ())])
return await future

async def get_firmware_async(self):
"""Get the firmware version of the device."""
future = self._loop.create_future()
self.enqueue(future, commands=[("GetFirmware", ())])
return await future

async def get_signal_quality_async(self):
"""Get signal quality from phone."""
future = self._loop.create_future()
self.enqueue(future, commands=[("GetSignalQuality", ())])
result = await future
return result

async def send_sms_async(self, message):
"""Send sms message via the phone."""
future = self._loop.create_future()
self.enqueue(future, commands=[("SendSMS", [message])])
result = await future
return result

async def set_incoming_callback_async(self, callback):
"""Set the callback to call from phone."""
future = self._loop.create_future()
self.enqueue(future, commands=[("SetIncomingCallback", [callback])])
result = await future
return result

async def set_incoming_sms_async(self):
"""Activate SMS notifications from phone."""
future = self._loop.create_future()
self.enqueue(future, commands=[("SetIncomingSMS", ())])
result = await future
return result

async def terminate_async(self):
"""Terminate phone communication."""
self._terminate_future = self._loop.create_future()
self.enqueue("Terminate")
await self._terminate_future

while self._thread.is_alive():
await asyncio.sleep(5)
self._thread = None
89 changes: 89 additions & 0 deletions test/test_asyncworker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# -*- coding: UTF-8 -*-
# vim: expandtab sw=4 ts=4 sts=4:
#
# Copyright © 2003 - 2018 Michal Čihař <michal@cihar.com>
#
# This file is part of python-gammu <https://wammu.eu/python-gammu/>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
import asyncio
import gammu.asyncworker
from .test_dummy import DummyTest


WORKER_EXPECT = [
('Init', None),
('GetIMEI', '999999999999999'),
('GetManufacturer', 'Gammu'),
('GetNetworkInfo',
{'CID': 'FACE',
'GPRS': 'Attached',
'LAC': 'B00B',
'NetworkCode': '999 99',
'NetworkName': '',
'PacketCID': 'DEAD',
'PacketLAC': 'BEEF',
'PacketState': 'HomeNetwork',
'State': 'HomeNetwork'}),
('GetModel', ('unknown', 'Dummy')),
#('GetFirmware', ('1.41.0', '20150101', 1.41)), # Mock is returning different values between the local workstation on the CI build
('GetSignalQuality', {'BitErrorRate': 0, 'SignalPercent': 42, 'SignalStrength': 42}),
('SendSMS', 255),
('SetIncomingCallback', None),
('SetIncomingSMS',None),
('Terminate', None)
]

def async_test(coro):
def wrapper(*args, **kwargs):
loop = asyncio.new_event_loop()
return loop.run_until_complete(coro(*args, **kwargs))
return wrapper

class AsyncWorkerDummyTest(DummyTest):
results = []

def callback(self, name, result, error, percents):
self.results.append((name, result, error, percents))

@async_test
async def test_worker_async(self):
self.results = []
worker = gammu.asyncworker.GammuAsyncWorker()
worker.configure(self.get_statemachine().GetConfig())
self.results.append(('Init', await worker.init_async()))
self.results.append(('GetIMEI', await worker.get_imei_async()))
self.results.append(('GetManufacturer', await worker.get_manufacturer_async()))
self.results.append(('GetNetworkInfo', await worker.get_network_info_async()))
self.results.append(('GetModel', await worker.get_model_async()))
#self.results.append(('GetFirmware', await worker.get_firmware_async()))
self.results.append(('GetSignalQuality', await worker.get_signal_quality_async()))
message = {
'Text': 'python-gammu testing message',
'SMSC': {'Location': 1},
'Number': '555-555-1234',
}
self.results.append(('SendSMS', await worker.send_sms_async(message)))
with self.assertRaises(TypeError):
await worker.send_sms_async(42)
with self.assertRaises(Exception):
await worker.send_sms_async(dict(42))
self.results.append(('SetIncomingCallback', await worker.set_incoming_callback_async(self.callback)))
self.results.append(('SetIncomingSMS', await worker.set_incoming_sms_async()))
self.results.append(('Terminate', await worker.terminate_async()))
self.maxDiff = None
self.assertEqual(WORKER_EXPECT, self.results)

0 comments on commit 13927d8

Please sign in to comment.