Skip to content

Commit b43d57b

Browse files
authored
Merge pull request #55 from adafruit/api-v2
V2 Update
2 parents f115ab5 + dccce92 commit b43d57b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1725
-724
lines changed

.travis.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# .travis.yml for Adafruit IO Python Client Library
2+
language: python
3+
dist: trusty
4+
sudo: required
5+
6+
python:
7+
- "3.6"
8+
9+
cache:
10+
pip: true
11+
12+
13+
install:
14+
- python setup.py install
15+
- pip install pylint Sphinx sphinx-rtd-theme
16+
- pip install .
17+
18+
script:
19+
- cd docs && sphinx-build -E -W -b html . _build/html
20+
- cd ..
21+
- cd tests/
22+
- git checkout api-v2
23+
- python -m unittest discover

Adafruit_IO/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@
2020
# SOFTWARE.
2121
from .client import Client
2222
from .mqtt_client import MQTTClient
23-
from .errors import AdafruitIOError, RequestError, ThrottlingError
24-
from .model import Data, Stream, Feed, Group
23+
from .errors import AdafruitIOError, RequestError, ThrottlingError, MQTTError
24+
from .model import Data, Feed, Group
25+
from ._version import __version__

Adafruit_IO/_version.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__version__ = "2.0.0"

Adafruit_IO/client.py

Lines changed: 57 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2014 Adafruit Industries
1+
# Copyright (c) 2018 Adafruit Industries
22
# Authors: Justin Cooper & Tony DiCola
33

44
# Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -21,6 +21,7 @@
2121
import json
2222
import pkg_resources
2323
import platform
24+
# import logging
2425

2526
import requests
2627

@@ -41,23 +42,30 @@ class Client(object):
4142
REST API. Use this client class to send, receive, and enumerate feed data.
4243
"""
4344

44-
def __init__(self, key, proxies=None, base_url='https://io.adafruit.com', api_version='v1'):
45+
def __init__(self, username, key, proxies=None, base_url='https://io.adafruit.com', api_version = 'v2'):
4546
"""Create an instance of the Adafruit IO REST API client. Key must be
4647
provided and set to your Adafruit IO access key value. Optionaly
4748
provide a proxies dict in the format used by the requests library, a
4849
base_url to point at a different Adafruit IO service (the default is
4950
the production Adafruit IO service over SSL), and a api_version to
5051
add support for future API versions.
5152
"""
53+
self.username = username
5254
self.key = key
5355
self.proxies = proxies
5456
self.api_version = api_version
57+
# self.logger = logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
58+
5559
# Save URL without trailing slash as it will be added later when
5660
# constructing the path.
5761
self.base_url = base_url.rstrip('/')
5862

59-
def _compose_url(self, path):
60-
return '{0}/api/{1}/{2}'.format(self.base_url, self.api_version, path)
63+
def _compose_url(self, path, is_time=None):
64+
if not is_time:
65+
return '{0}/api/{1}/{2}/{3}'.format(self.base_url, self.api_version, self.username, path)
66+
else: # return a call to https://io.adafruit.com/api/v2/time/{unit}
67+
return '{0}/api/{1}/{2}'.format(self.base_url, self.api_version, path)
68+
6169

6270
def _handle_error(self, response):
6371
# Handle explicit errors.
@@ -73,12 +81,15 @@ def _headers(self, given):
7381
headers.update(given)
7482
return headers
7583

76-
def _get(self, path):
77-
response = requests.get(self._compose_url(path),
84+
def _get(self, path, is_time=None):
85+
response = requests.get(self._compose_url(path, is_time),
7886
headers=self._headers({'X-AIO-Key': self.key}),
7987
proxies=self.proxies)
8088
self._handle_error(response)
81-
return response.json()
89+
if not is_time:
90+
return response.json()
91+
else: # time doesn't need to serialize into json, just return text
92+
return response.text
8293

8394
def _post(self, path, data):
8495
response = requests.post(self._compose_url(path),
@@ -97,14 +108,25 @@ def _delete(self, path):
97108
self._handle_error(response)
98109

99110
# Data functionality.
100-
def send(self, feed_name, value):
101-
"""Helper function to simplify adding a value to a feed. Will find the
102-
specified feed by name or create a new feed if it doesn't exist, then
103-
will append the provided value to the feed. Returns a Data instance
104-
with details about the newly appended row of data.
111+
def send_data(self, feed, value):
112+
"""Helper function to simplify adding a value to a feed. Will append the
113+
specified value to the feed identified by either name, key, or ID.
114+
Returns a Data instance with details about the newly appended row of data.
115+
Note that send_data now operates the same as append.
105116
"""
106-
path = "feeds/{0}/data/send".format(feed_name)
107-
return Data.from_dict(self._post(path, {'value': value}))
117+
return self.create_data(feed, Data(value=value))
118+
119+
send = send_data
120+
121+
def send_batch_data(self, feed, data_list):
122+
"""Create a new row of data in the specified feed. Feed can be a feed
123+
ID, feed key, or feed name. Data must be an instance of the Data class
124+
with at least a value property set on it. Returns a Data instance with
125+
details about the newly appended row of data.
126+
"""
127+
path = "feeds/{0}/data/batch".format(feed)
128+
data_dict = type(data_list)((data._asdict() for data in data_list))
129+
self._post(path, {"data": data_dict})
108130

109131
def append(self, feed, value):
110132
"""Helper function to simplify adding a value to a feed. Will append the
@@ -114,6 +136,26 @@ def append(self, feed, value):
114136
"""
115137
return self.create_data(feed, Data(value=value))
116138

139+
def send_location_data(self, feed, value, lat, lon, ele):
140+
"""Sends locational data to a feed
141+
142+
args:
143+
- lat: latitude
144+
- lon: logitude
145+
- ele: elevation
146+
- (optional) value: value to send to the feed
147+
"""
148+
return self.create_data(feed, Data(value = value,lat=lat, lon=lon, ele=ele))
149+
150+
def receive_time(self, time):
151+
"""Returns the time from the Adafruit IO server.
152+
153+
args:
154+
- time (string): millis, seconds, ISO-8601
155+
"""
156+
timepath = "time/{0}".format(time)
157+
return self._get(timepath, is_time=True)
158+
117159
def receive(self, feed):
118160
"""Retrieve the most recent value for the specified feed. Feed can be a
119161
feed ID, feed key, or feed name. Returns a Data instance whose value
@@ -185,7 +227,7 @@ def create_feed(self, feed):
185227
type with at least the name property set.
186228
"""
187229
path = "feeds/"
188-
return Feed.from_dict(self._post(path, feed._asdict()))
230+
return Feed.from_dict(self._post(path, {"feed": feed._asdict()}))
189231

190232
def delete_feed(self, feed):
191233
"""Delete the specified feed. Feed can be a feed ID, feed key, or feed
@@ -194,25 +236,6 @@ def delete_feed(self, feed):
194236
path = "feeds/{0}".format(feed)
195237
self._delete(path)
196238

197-
# Group functionality.
198-
def send_group(self, group_name, data):
199-
"""Update all feeds in a group with one call. Group_name should be the
200-
name of a group to update. Data should be a dict with an item for each
201-
feed in the group, where the key is the feed name and value is the new
202-
data row value. For example a group 'TestGroup' with feeds 'FeedOne'
203-
and 'FeedTwo' could be updated by calling:
204-
205-
send_group('TestGroup', {'FeedOne': 'value1', 'FeedTwo': 10})
206-
207-
This would add the value 'value1' to the feed 'FeedOne' and add the
208-
value 10 to the feed 'FeedTwo'.
209-
210-
After a successful update an instance of Group will be returned with
211-
metadata about the updated group.
212-
"""
213-
path = "groups/{0}/send".format(group_name)
214-
return Group.from_dict(self._post(path, {'value': data}))
215-
216239
def receive_group(self, group):
217240
"""Retrieve the most recent value for the specified group. Group can be
218241
a group ID, group key, or group name. Returns a Group instance whose

Adafruit_IO/errors.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@
1818
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1919
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2020
# SOFTWARE.
21+
22+
import json
23+
24+
# MQTT RC Error Types
25+
MQTT_ERRORS = [ 'Connection successful',
26+
'Incorrect protocol version',
27+
'Invalid Client ID',
28+
'Server unavailable ',
29+
'Bad username or password',
30+
'Not authorized' ]
31+
2132
class AdafruitIOError(Exception):
2233
"""Base class for all Adafruit IO request failures."""
2334
pass
@@ -26,8 +37,16 @@ class AdafruitIOError(Exception):
2637
class RequestError(Exception):
2738
"""General error for a failed Adafruit IO request."""
2839
def __init__(self, response):
29-
super(RequestError, self).__init__("Adafruit IO request failed: {0} {1}".format(
30-
response.status_code, response.reason))
40+
error_message = self._parse_error(response)
41+
super(RequestError, self).__init__("Adafruit IO request failed: {0} {1} - {2}".format(
42+
response.status_code, response.reason, error_message))
43+
44+
def _parse_error(self, response):
45+
try:
46+
content = json.loads(response.content)
47+
return ' - '.join(content['error'])
48+
except ValueError:
49+
return ""
3150

3251

3352
class ThrottlingError(AdafruitIOError):
@@ -38,3 +57,12 @@ def __init__(self):
3857
super(ThrottlingError, self).__init__("Exceeded the limit of Adafruit IO " \
3958
"requests in a short period of time. Please reduce the rate of requests " \
4059
"and try again later.")
60+
61+
62+
class MQTTError(Exception):
63+
"""Handles connection attempt failed errors.
64+
"""
65+
def __init__(self, response):
66+
error = MQTT_ERRORS[response]
67+
super(MQTTError, self).__init__(error)
68+
pass

Adafruit_IO/model.py

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,33 +28,29 @@
2828

2929

3030
# List of fields/properties that are present on a data object from IO.
31-
DATA_FIELDS = [ 'created_epoch',
31+
DATA_FIELDS = [ 'created_epoch',
3232
'created_at',
3333
'updated_at',
3434
'value',
3535
'completed_at',
3636
'feed_id',
3737
'expiration',
3838
'position',
39-
'id' ]
40-
41-
STREAM_FIELDS = [ 'completed_at',
42-
'created_at',
4339
'id',
44-
'value' ]
40+
'lat',
41+
'lon',
42+
'ele']
4543

46-
FEED_FIELDS = [ 'last_value_at',
47-
'name',
48-
'stream',
49-
'created_at',
50-
'updated_at',
51-
'unit_type',
52-
'mode',
44+
FEED_FIELDS = [ 'name',
5345
'key',
46+
'description',
47+
'unit_type',
5448
'unit_symbol',
55-
'fixed',
56-
'last_value',
57-
'id' ]
49+
'history',
50+
'visibility',
51+
'license',
52+
'status_notify',
53+
'status_timeout']
5854

5955
GROUP_FIELDS = [ 'description',
6056
'source_keys',
@@ -73,18 +69,17 @@
7369
# client it might be prudent to revisit this decision and consider making these
7470
# full fledged classes that are mutable.
7571
Data = namedtuple('Data', DATA_FIELDS)
76-
Stream = namedtuple('Stream', STREAM_FIELDS)
7772
Feed = namedtuple('Feed', FEED_FIELDS)
7873
Group = namedtuple('Group', GROUP_FIELDS)
7974

8075

8176
# Magic incantation to make all parameters to the initializers optional with a
8277
# default value of None.
83-
Data.__new__.__defaults__ = tuple(None for x in DATA_FIELDS)
84-
Stream.__new__.__defaults__ = tuple(None for x in STREAM_FIELDS)
85-
Feed.__new__.__defaults__ = tuple(None for x in FEED_FIELDS)
8678
Group.__new__.__defaults__ = tuple(None for x in GROUP_FIELDS)
79+
Data.__new__.__defaults__ = tuple(None for x in DATA_FIELDS)
8780

81+
# explicitly set feed values
82+
Feed.__new__.__defaults__ = (None, None, None, None, None, 'ON', 'Private', None, None, None)
8883

8984
# Define methods to convert from dicts to the data types.
9085
def _from_dict(cls, data):
@@ -98,8 +93,6 @@ def _from_dict(cls, data):
9893

9994
def _feed_from_dict(cls, data):
10095
params = {x: data.get(x, None) for x in cls._fields}
101-
# Parse the stream if provided and generate a stream instance.
102-
params['stream'] = Stream.from_dict(data.get('stream', {}))
10396
return cls(**params)
10497

10598

@@ -112,6 +105,5 @@ def _group_from_dict(cls, data):
112105

113106
# Now add the from_dict class methods defined above to the data types.
114107
Data.from_dict = classmethod(_from_dict)
115-
Stream.from_dict = classmethod(_from_dict)
116108
Feed.from_dict = classmethod(_feed_from_dict)
117109
Group.from_dict = classmethod(_group_from_dict)

0 commit comments

Comments
 (0)