Skip to content

Commit 7fe0b78

Browse files
committed
Hologram Python SDK v0.6.0 release
1 parent 916da89 commit 7fe0b78

26 files changed

+591
-178
lines changed

ChangeLog

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
2017-09-22 Hologram <[email protected]>
2+
* Added program install verification steps to install script.
3+
4+
2017-09-20 Hologram <[email protected]>
5+
* Added hologram activate CLI subcommand. This allows developers to
6+
activate their SIM via the CLI instead of doing it on the dashboard.
7+
8+
2017-09-18 Hologram <[email protected]>
9+
* Added autodetection of USB modems and serial ports associated with those
10+
modems
11+
* Fix issue where scripts might hang when trying to use a serial port with
12+
ppp attached
13+
* Remove some redundant checks and unneeded initializations that were slowing
14+
things down
15+
116
2017-09-12 Hologram <[email protected]>
217
* Deprecated enable_inbound flag in HologramCloud constructor.
318
* Suppress stack traces when user sends a SIGTERM while it's establishing

Exceptions/HologramError.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ class HologramError(Exception):
33
def __repr__(self):
44
return '%s: %s' % (type(self).__name__, str(self))
55

6+
class ApiError(HologramError):
7+
pass
8+
69
class AuthenticationError(HologramError):
710
pass
811

Hologram/Api/Api.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
from Exceptions.HologramError import ApiError
2+
3+
import logging
4+
from logging import NullHandler
5+
import requests
6+
7+
HOLOGRAM_REST_API_BASEURL = 'https://dashboard.hologram.io/api/1'
8+
9+
class Api(object):
10+
11+
def __init__(self, apikey='', username='', password=''):
12+
# Logging setup.
13+
self.logger = logging.getLogger(__name__)
14+
self.logger.addHandler(NullHandler())
15+
self.authtype = None
16+
17+
self.__enforce_auth_method(apikey, username, password)
18+
19+
self.apikey = apikey
20+
self.username = username
21+
self.password = password
22+
23+
# REQUIRES: a SIM number and a plan id.
24+
# EFFECTS: Activates a SIM. Returns a tuple of a success flag and
25+
# more info about the response.
26+
def activateSIM(self, sim='', plan=None, zone=1, preview=False):
27+
28+
endpoint = HOLOGRAM_REST_API_BASEURL + '/links/cellular/sim_' + str(sim) + '/claim'
29+
30+
args = self.__populate_auth_payload()
31+
args['data'] = {'plan': plan, 'tier': zone}
32+
33+
if preview == True:
34+
args['params']['preview'] = 1
35+
36+
response = requests.post(endpoint, **args)
37+
if response.status_code != requests.codes.ok:
38+
response.raise_for_status()
39+
40+
response = response.json()
41+
if response['success'] == False:
42+
return (response['success'], response['data'][str(sim)])
43+
return (response['success'], response['order_data'])
44+
45+
# EFFECTS: Returns a list of plans. Returns a tuple of a success flag and
46+
# more info about the response.
47+
def getPlans(self):
48+
endpoint = HOLOGRAM_REST_API_BASEURL + '/plans'
49+
args = self.__populate_auth_payload()
50+
51+
response = requests.get(endpoint, **args)
52+
if response.status_code != requests.codes.ok:
53+
response.raise_for_status()
54+
55+
response = response.json()
56+
return (response['success'], response['data'])
57+
58+
# EFFECTS: Gets the SIM state
59+
def getSIMState(self, sim):
60+
endpoint = HOLOGRAM_REST_API_BASEURL + '/links/cellular'
61+
62+
args = self.__populate_auth_payload()
63+
args['params']['sim'] = str(sim)
64+
65+
response = requests.get(endpoint, **args)
66+
if response.status_code != requests.codes.ok:
67+
response.raise_for_status()
68+
69+
response = response.json()
70+
return (response['success'], response['data'][0]['state'])
71+
72+
# EFFECTS: Populates and returns a dictionary with the proper HTTP
73+
# authentication credentials.
74+
def __populate_auth_payload(self):
75+
76+
args = dict()
77+
args['params'] = dict()
78+
79+
if self.authtype == 'basic_auth':
80+
args['auth'] = (self.username, self.password)
81+
elif self.authtype == 'apikey':
82+
args['params'] = {'apikey' : self.apikey}
83+
else:
84+
raise ApiError('Invalid HTTP Authentication type')
85+
86+
return args
87+
88+
# EFFECTS: Checks to make sure that the valid authentication parameters are being used
89+
# correctly, throws an Exception if there's an issue with it.
90+
def __enforce_auth_method(self, apikey, username, password):
91+
if apikey == '' and (username == '' or password == ''):
92+
raise ApiError('Must specify valid HTTP authentication credentials')
93+
elif apikey == '':
94+
self.authtype = 'basic_auth'
95+
else:
96+
self.authtype = 'apikey'

Hologram/Api/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from Api import *

Hologram/Cloud.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from Network import NetworkManager
1515
from Authentication import *
1616

17-
__version__ = '0.5.28'
17+
__version__ = '0.6.0'
1818

1919
class Cloud(object):
2020

Hologram/CustomCloud.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,9 @@ def stopPeriodicMessage(self):
207207
self.logger.info('Periodic job stopped')
208208

209209
def initializeReceiveSocket(self):
210+
return self.openReceiveSocket()
211+
212+
def openReceiveSocket(self):
210213

211214
try:
212215
self.__enforce_receive_host_and_port()
@@ -220,12 +223,12 @@ def initializeReceiveSocket(self):
220223
self.logger.info('Socket created')
221224
self._receive_cv.release()
222225

223-
self.openReceiveSocket()
226+
self.open_receive_socket_helper()
224227

225228
return True
226229

227230
# EFFECTS: Opens and binds an inbound socket connection.
228-
def openReceiveSocket(self):
231+
def open_receive_socket_helper(self):
229232

230233
self._receive_cv.acquire()
231234

Hologram/HologramCloud.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ def sendMessage(self, message, topics = None, timeout = 5):
127127
def sendSMS(self, destination_number, message):
128128

129129
try:
130+
self.__enforce_valid_destination_number(destination_number)
130131
self.__enforce_max_sms_length(message)
131132
except HologramError as e:
132133
self.logger.error(repr(e))
@@ -211,6 +212,10 @@ def __enforce_max_sms_length(self, message):
211212
if len(message) > MAX_SMS_LENGTH:
212213
raise HologramError('SMS cannot be more than %d characters long' % MAX_SMS_LENGTH)
213214

215+
def __enforce_valid_destination_number(self, destination_number):
216+
if not destination_number.startswith('+'):
217+
raise HologramError('SMS destination number must start with a \'+\' sign')
218+
214219
# REQUIRES: A result code (int).
215220
# EFFECTS: Returns a translated string based on the given hologram result code.
216221
def getResultString(self, result_code):

Hologram/Network/Cellular.py

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from Network import Network
1818
import subprocess
1919
import sys
20+
import usb.core
2021

2122
# Cellular return codes.
2223
CLOUD_DISCONNECTED = 0
@@ -36,18 +37,25 @@ class Cellular(Network):
3637
'': Modem
3738
}
3839

39-
def __init__(self, modem='', event=Event()):
40+
def __init__(self, event=Event()):
4041
super(Cellular, self).__init__(event=event)
4142
self._connectionStatus = CLOUD_DISCONNECTED
42-
self.modem = modem
43+
self._modem = None
44+
45+
46+
def autodetect_modem(self):
47+
# scan for a modem and set it if found
48+
dev_devices = self._scan_for_modems()
49+
if dev_devices is None:
50+
raise NetworkError('Modem not detected')
51+
self.modem = dev_devices[0]
52+
4353

4454
def getConnectionStatus(self):
4555
return self._connectionStatus
4656

4757
def connect(self, timeout = DEFAULT_CELLULAR_TIMEOUT):
48-
self.logger.info('Connecting to cell network with timeout of ' + str(timeout) + ' seconds')
49-
self._enforce_modem_attached()
50-
58+
self.logger.info('Connecting to cell network with timeout of %s seconds', timeout)
5159
success = False
5260
try:
5361
success = self.modem.connect(timeout = timeout)
@@ -66,7 +74,6 @@ def connect(self, timeout = DEFAULT_CELLULAR_TIMEOUT):
6674

6775
def disconnect(self):
6876
self.logger.info('Disconnecting from cell network')
69-
self._enforce_modem_attached()
7077
success = self.modem.disconnect()
7178
if success:
7279
self.logger.info('Successfully disconnected from cell network')
@@ -101,33 +108,32 @@ def popReceivedSMS(self):
101108
def get_sim_otp_response(self, command):
102109
return self.modem.get_sim_otp_response(command)
103110

104-
# EFFECTS: Returns a list of devices that are physically attached and recognized
105-
# by the machine.
106-
def _get_attached_devices(self):
107-
return subprocess.check_output('ls /dev/tty*', stderr=subprocess.STDOUT,
108-
shell=True)
109-
def _get_active_device_name(self):
110-
111-
self._enforce_modem_attached()
112-
113-
dev_devices = self._get_attached_devices()
114-
if '/dev/ttyACM0' in dev_devices:
115-
self.logger.info('/dev/ttyACM0 found to be active modem interface')
116-
return 'nova'
117-
elif '/dev/ttyUSB0' in dev_devices:
118-
self.logger.info('/dev/ttyUSB0 found to be active modem interface')
119-
return 'ms2131'
120-
else:
121-
raise NetworkError('Modem device name not found')
122111

123-
def _enforce_modem_attached(self):
124-
if self.isModemAttached() == False:
125-
raise NetworkError('Modem is not physically connected')
112+
def _scan_for_modems(self):
113+
res = None
114+
for (modemName, modemHandler) in self._modemHandlers.iteritems():
115+
if self._scan_for_modem(modemHandler):
116+
res = (modemName, modemHandler)
117+
break
118+
return res
119+
120+
121+
def _scan_for_modem(self, modemHandler):
122+
usb_ids = modemHandler.usb_ids
123+
for vid_pid in usb_ids:
124+
if not vid_pid:
125+
continue
126+
self.logger.debug('checking for vid_pid: %s', str(vid_pid))
127+
vid = int(vid_pid[0], 16)
128+
pid = int(vid_pid[1], 16)
129+
dev = usb.core.find(idVendor=vid, idProduct=pid)
130+
if dev:
131+
self.logger.info('Detected modem %s', modemHandler.__name__)
132+
return True
133+
return False
134+
135+
126136

127-
# EFFECTS: Returns True if a supported modem is physically attached to the machine.
128-
def isModemAttached(self):
129-
dev_devices = self._get_attached_devices()
130-
return ('/dev/ttyACM0' in dev_devices) or ('/dev/ttyUSB0' in dev_devices)
131137

132138
@property
133139
def modem(self):
@@ -136,11 +142,11 @@ def modem(self):
136142
@modem.setter
137143
def modem(self, modem):
138144
try:
139-
modem = self._get_active_device_name()
140145
if modem not in self._modemHandlers:
141146
raise NetworkError('Invalid modem type: %s' % modem)
142147
else:
143148
self._modem = self._modemHandlers[modem](event=self.event)
149+
# not sure about the exception handling in here. seems like we should let it bubble up
144150
except NetworkError as e:
145151
self.logger.error(repr(e))
146152
sys.exit(1)

Hologram/Network/Modem/E303.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,17 @@
1111
from Modem import Modem
1212
from ...Event import Event
1313

14-
E303_DEVICE_NAME = '/dev/ttyUSB0'
1514
DEFAULT_E303_TIMEOUT = 200
1615

1716
class E303(Modem):
17+
usb_ids = [('12d1','1001')]
1818

19-
def __init__(self, device_name=E303_DEVICE_NAME, baud_rate='9600',
19+
def __init__(self, device_name=None, baud_rate='9600',
2020
chatscript_file=None, event=Event()):
2121

2222
super(E303, self).__init__(device_name=device_name, baud_rate=baud_rate,
2323
chatscript_file=chatscript_file, event=event)
2424

25-
def isConnected(self):
26-
return self._mode.connected()
25+
def connect(self, timeout = DEFAULT_E303_TIMEOUT):
26+
return super(E303, self).connect(timeout)
2727

28-
def connect(self, timeout=DEFAULT_E303_TIMEOUT):
29-
return self._mode.connect(timeout=timeout)
30-
31-
def disconnect(self):
32-
return self._mode.disconnect()

Hologram/Network/Modem/IModem.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
class IModem(object):
2121

22+
usb_ids = []
23+
2224
_error_code_description = {
2325

2426
MODEM_NO_MATCH: 'Modem response doesn\'t match expected return value',
@@ -34,6 +36,7 @@ def __init__(self, device_name='/dev/ttyUSB0', baud_rate='9600', event=Event()):
3436

3537
self.event = event
3638
self.device_name = device_name
39+
self.baud_rate = baud_rate
3740

3841
def __repr__(self):
3942
return type(self).__name__

0 commit comments

Comments
 (0)