Major update:
- Refactored REST client to use requests library for Python 3 compatibility. - Added docstrings to all public methods & classes. - Fleshed out CRUD APIs for data, feed, group. - Wrote full suite of integration tests to verify changes. - Updated setup.py to bump version to 0.9 & depend on requests and paho-mqtt modules. - Updated and tested code to work with both Python 2 and 3. - Broke out errors into separate file, and added explicit data model classes. - General cleanup and preparation for public release.
This commit is contained in:
parent
2ee81791a2
commit
9900d508f5
19 changed files with 1354 additions and 361 deletions
|
|
@ -1,2 +1,24 @@
|
|||
from .client import Client, AdafruitIOError, RequestError, ThrottlingError, Data
|
||||
from .mqtt_client import MQTTClient
|
||||
# Copyright (c) 2014 Adafruit Industries
|
||||
# Author: Tony DiCola
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
from .client import Client
|
||||
from .mqtt_client import MQTTClient
|
||||
from .errors import AdafruitIOError, RequestError, ThrottlingError
|
||||
from .model import Data, Stream, Feed, Group
|
||||
|
|
|
|||
|
|
@ -1,216 +1,242 @@
|
|||
import collections
|
||||
# Copyright (c) 2014 Adafruit Industries
|
||||
# Authors: Justin Cooper & Tony DiCola
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import json
|
||||
|
||||
from urllib3 import connection_from_url
|
||||
from urllib import urlencode, quote
|
||||
import requests
|
||||
|
||||
from .errors import RequestError, ThrottlingError
|
||||
from .model import Data, Feed, Group
|
||||
|
||||
|
||||
class AdafruitIOError(Exception):
|
||||
"""Base class for all Adafruit IO request failures."""
|
||||
pass
|
||||
|
||||
|
||||
class RequestError(Exception):
|
||||
"""General error for a failed Adafruit IO request."""
|
||||
def __init__(self, response):
|
||||
super(RequestError, self).__init__("Adafruit IO request failed: {0} {1}".format(
|
||||
response.status, response.reason))
|
||||
|
||||
|
||||
class ThrottlingError(AdafruitIOError):
|
||||
"""Too many requests have been made to Adafruit IO in a short period of time.
|
||||
Reduce the rate of requests and try again later.
|
||||
"""
|
||||
def __init__(self):
|
||||
super(ThrottlingError, self).__init__("Exceeded the limit of Adafruit IO " \
|
||||
"requests in a short period of time. Please reduce the rate of requests " \
|
||||
"and try again later.")
|
||||
|
||||
|
||||
class Data(collections.namedtuple('Data', ['created_epoch', 'created_at',
|
||||
'updated_at', 'value', 'completed_at', 'feed_id', 'expiration', 'position',
|
||||
'id'])):
|
||||
"""Row of data from a feed. This is a simple class that just represents data
|
||||
returned from the Adafruit IO service. The value property has the value of the
|
||||
data row, and other properties like created_at or id represent the metadata
|
||||
of the data row.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def from_response(cls, response):
|
||||
"""Create a new Data instance based on the response dict from an Adafruit IO
|
||||
request.
|
||||
"""
|
||||
# Be careful to support forward compatibility by only looking at attributes
|
||||
# which this Data class knows about (i.e. ignore anything else that's
|
||||
# unknown).
|
||||
# In this case iterate through all the fields of the named tuple and grab
|
||||
# their value from the response dict. If any field doesn't exist in the
|
||||
# response dict then set it to None.
|
||||
return cls(*[response.get(x, None) for x in cls._fields])
|
||||
|
||||
# Magic incantation to make all parameters to the constructor optional with a
|
||||
# default value of None. This is useful when creating an explicit Data instance
|
||||
# to pass to create_data with only the value or other properties set.
|
||||
Data.__new__.__defaults__ = tuple(None for x in Data._fields)
|
||||
|
||||
|
||||
#fork of ApiClient Class: https://github.com/shazow/apiclient
|
||||
class Client(object):
|
||||
"""Client instance for interacting with the Adafruit IO service using its REST
|
||||
API. Use this client class to send, receive, and enumerate feed data.
|
||||
"""
|
||||
BASE_URL = 'https://io.adafruit.com/'
|
||||
|
||||
def __init__(self, key, rate_limit_lock=None):
|
||||
"""Create an instance of the Adafruit IO REST API client. Key must be
|
||||
provided and set to your Adafruit IO access key value.
|
||||
"""Client instance for interacting with the Adafruit IO service using its
|
||||
REST API. Use this client class to send, receive, and enumerate feed data.
|
||||
"""
|
||||
self.key = key
|
||||
self.rate_limit_lock = rate_limit_lock
|
||||
self.connection_pool = self._make_connection_pool(self.BASE_URL)
|
||||
|
||||
def _make_connection_pool(self, url):
|
||||
return connection_from_url(url)
|
||||
def __init__(self, key, proxies=None, base_url='https://io.adafruit.com'):
|
||||
"""Create an instance of the Adafruit IO REST API client. Key must be
|
||||
provided and set to your Adafruit IO access key value. Optionaly
|
||||
provide a proxies dict in the format used by the requests library, and
|
||||
base_url to point at a different Adafruit IO service (the default is
|
||||
the production Adafruit IO service over SSL).
|
||||
"""
|
||||
self.key = key
|
||||
self.proxies = proxies
|
||||
# Save URL without trailing slash as it will be added later when
|
||||
# constructing the path.
|
||||
self.base_url = base_url.rstrip('/')
|
||||
|
||||
def _compose_url(self, path):
|
||||
return self.BASE_URL + path
|
||||
def _compose_url(self, path):
|
||||
return '{0}/{1}'.format(self.base_url, path)
|
||||
|
||||
def _compose_get_url(self, path, params=None):
|
||||
return self.BASE_URL + path + '?' + urlencode(params)
|
||||
def _handle_error(sefl, response):
|
||||
# Handle explicit errors.
|
||||
if response.status_code == 429:
|
||||
raise ThrottlingError()
|
||||
# Handle all other errors (400 & 500 level HTTP responses)
|
||||
elif response.status_code >= 400:
|
||||
raise RequestError(response)
|
||||
# Else do nothing if there was no error.
|
||||
|
||||
def _handle_error(sefl, response):
|
||||
# Handle explicit errors.
|
||||
if response.status == 429:
|
||||
raise ThrottlingError()
|
||||
# Handle all other errors (400 & 500 level HTTP responses)
|
||||
elif response.status >= 400:
|
||||
raise RequestError(response)
|
||||
# Else do nothing if there was no error.
|
||||
def _get(self, path):
|
||||
response = requests.get(self._compose_url(path),
|
||||
headers={'X-AIO-Key': self.key},
|
||||
proxies=self.proxies)
|
||||
self._handle_error(response)
|
||||
return response.json()
|
||||
|
||||
def _handle_response(self, response, expect_result):
|
||||
self._handle_error(response)
|
||||
if expect_result:
|
||||
return json.loads(response.data)
|
||||
# Else no result expected so just return.
|
||||
def _post(self, path, data):
|
||||
response = requests.post(self._compose_url(path),
|
||||
headers={'X-AIO-Key': self.key,
|
||||
'Content-Type': 'application/json'},
|
||||
proxies=self.proxies,
|
||||
data=json.dumps(data))
|
||||
self._handle_error(response)
|
||||
return response.json()
|
||||
|
||||
def _request(self, method, path, params=None, expect_result=True):
|
||||
if (method.lower() == "get"):
|
||||
url = self._compose_get_url(path, params)
|
||||
else:
|
||||
url = self._compose_url(path)
|
||||
def _delete(self, path):
|
||||
response = requests.delete(self._compose_url(path),
|
||||
headers={'X-AIO-Key': self.key,
|
||||
'Content-Type': 'application/json'},
|
||||
proxies=self.proxies)
|
||||
self._handle_error(response)
|
||||
|
||||
self.rate_limit_lock and self.rate_limit_lock.acquire()
|
||||
headers = {"X-AIO-Key": self.key, 'Content-Type':'application/json'}
|
||||
if (method.upper() == "GET"):
|
||||
r = self.connection_pool.urlopen(method.upper(), url, headers=headers)
|
||||
else:
|
||||
r = self.connection_pool.urlopen(method.upper(), url, headers=headers,
|
||||
body=json.dumps(params))
|
||||
# Data functionality.
|
||||
def send(self, feed_name, value):
|
||||
"""Helper function to simplify adding a value to a feed. Will find the
|
||||
specified feed by name or create a new feed if it doesn't exist, then
|
||||
will append the provided value to the feed. Returns a Data instance
|
||||
with details about the newly appended row of data.
|
||||
"""
|
||||
path = "api/feeds/{0}/data/send".format(feed_name)
|
||||
return Data.from_dict(self._post(path, {'value': value}))
|
||||
|
||||
return self._handle_response(r, expect_result)
|
||||
def receive(self, feed):
|
||||
"""Retrieve the most recent value for the specified feed. Feed can be a
|
||||
feed ID, feed key, or feed name. Returns a Data instance whose value
|
||||
property holds the retrieved value.
|
||||
"""
|
||||
path = "api/feeds/{0}/data/last".format(feed)
|
||||
return Data.from_dict(self._get(path))
|
||||
|
||||
def _get(self, path, **params):
|
||||
return self._request('GET', path, params=params)
|
||||
def receive_next(self, feed):
|
||||
"""Retrieve the next unread value from the specified feed. Feed can be
|
||||
a feed ID, feed key, or feed name. Returns a Data instance whose value
|
||||
property holds the retrieved value.
|
||||
"""
|
||||
path = "api/feeds/{0}/data/next".format(feed)
|
||||
return Data.from_dict(self._get(path))
|
||||
|
||||
def _post(self, path, params):
|
||||
return self._request('POST', path, params=params)
|
||||
def receive_previous(self, feed):
|
||||
"""Retrieve the previous unread value from the specified feed. Feed can
|
||||
be a feed ID, feed key, or feed name. Returns a Data instance whose
|
||||
value property holds the retrieved value.
|
||||
"""
|
||||
path = "api/feeds/{0}/data/previous".format(feed)
|
||||
return Data.from_dict(self._get(path))
|
||||
|
||||
def _delete(self, path):
|
||||
return self._request('DELETE', path, expect_result=False)
|
||||
def data(self, feed, data_id=None):
|
||||
"""Retrieve data from a feed. Feed can be a feed ID, feed key, or feed
|
||||
name. Data_id is an optional id for a single data value to retrieve.
|
||||
If data_id is not specified then all the data for the feed will be
|
||||
returned in an array.
|
||||
"""
|
||||
if data_id is None:
|
||||
path = "api/feeds/{0}/data".format(feed)
|
||||
return list(map(Data.from_dict, self._get(path)))
|
||||
else:
|
||||
path = "api/feeds/{0}/data/{1}".format(feed, data_id)
|
||||
return Data.from_dict(self._get(path))
|
||||
|
||||
#feed functionality
|
||||
def delete_feed(self, feed):
|
||||
"""Delete the specified feed. Feed can be a feed ID, feed key, or feed name.
|
||||
"""
|
||||
feed = quote(feed)
|
||||
path = "api/feeds/{}".format(feed)
|
||||
self._delete(path)
|
||||
def create_data(self, feed, data):
|
||||
"""Create a new row of data in the specified feed. Feed can be a feed
|
||||
ID, feed key, or feed name. Data must be an instance of the Data class
|
||||
with at least a value property set on it. Returns a Data instance with
|
||||
details about the newly appended row of data.
|
||||
"""
|
||||
path = "api/feeds/{0}/data".format(feed)
|
||||
return Data.from_dict(self._post(path, data._asdict()))
|
||||
|
||||
#feed data functionality
|
||||
def send(self, feed_name, value):
|
||||
"""Helper function to simplify adding a value to a feed. Will find the
|
||||
specified feed by name or create a new feed if it doesn't exist, then will
|
||||
append the provided value to the feed. Returns a Data instance with details
|
||||
about the newly appended row of data.
|
||||
"""
|
||||
feed_name = quote(feed_name)
|
||||
path = "api/feeds/{}/data/send".format(feed_name)
|
||||
return Data.from_response(self._post(path, {'value': value}))
|
||||
def delete(self, feed, data_id):
|
||||
"""Delete data from a feed. Feed can be a feed ID, feed key, or feed
|
||||
name. Data_id must be the ID of the piece of data to delete.
|
||||
"""
|
||||
path = "api/feeds/{0}/data/{1}".format(feed, data_id)
|
||||
self._delete(path)
|
||||
|
||||
def receive(self, feed):
|
||||
"""Retrieve the most recent value for the specified feed. Feed can be a
|
||||
feed ID, feed key, or feed name. Returns a Data instance whose value
|
||||
property holds the retrieved value.
|
||||
"""
|
||||
feed = quote(feed)
|
||||
path = "api/feeds/{}/data/last".format(feed)
|
||||
return Data.from_response(self._get(path))
|
||||
# Feed functionality.
|
||||
def feeds(self, feed=None):
|
||||
"""Retrieve a list of all feeds, or the specified feed. If feed is not
|
||||
specified a list of all feeds will be returned. If feed is specified it
|
||||
can be a feed name, key, or ID and the requested feed will be returned.
|
||||
"""
|
||||
if feed is None:
|
||||
path = "api/feeds"
|
||||
return list(map(Feed.from_dict, self._get(path)))
|
||||
else:
|
||||
path = "api/feeds/{0}".format(feed)
|
||||
return Feed.from_dict(self._get(path))
|
||||
|
||||
def receive_next(self, feed):
|
||||
"""Retrieve the next unread value from the specified feed. Feed can be a
|
||||
feed ID, feed key, or feed name. Returns a Data instance whose value
|
||||
property holds the retrieved value.
|
||||
"""
|
||||
feed = quote(feed)
|
||||
path = "api/feeds/{}/data/next".format(feed)
|
||||
return Data.from_response(self._get(path))
|
||||
def create_feed(self, feed):
|
||||
"""Create the specified feed. Feed should be an instance of the Feed
|
||||
type with at least the name property set.
|
||||
"""
|
||||
path = "api/feeds/"
|
||||
return Feed.from_dict(self._post(path, feed._asdict()))
|
||||
|
||||
def receive_previous(self, feed):
|
||||
"""Retrieve the previously read value from the specified feed. Feed can be
|
||||
a feed ID, feed key, or feed name. Returns a Data instance whose value
|
||||
property holds the retrieved value.
|
||||
"""
|
||||
feed = quote(feed)
|
||||
path = "api/feeds/{}/data/last".format(feed)
|
||||
return Data.from_response(self._get(path))
|
||||
def delete_feed(self, feed):
|
||||
"""Delete the specified feed. Feed can be a feed ID, feed key, or feed
|
||||
name.
|
||||
"""
|
||||
path = "api/feeds/{0}".format(feed)
|
||||
self._delete(path)
|
||||
|
||||
def data(self, feed, data_id=None):
|
||||
"""Retrieve data from a feed. Feed can be a feed ID, feed key, or feed name.
|
||||
Data_id is an optional id for a single data value to retrieve. If data_id
|
||||
is not specified then all the data for the feed will be returned in an array.
|
||||
"""
|
||||
if data_id is None:
|
||||
path = "api/feeds/{}/data".format(feed)
|
||||
return map(Data.from_response, self._get(path))
|
||||
else:
|
||||
path = "api/feeds/{}/data/{}".format(feed, data_id)
|
||||
return Data.from_response(self._get(path))
|
||||
# Group functionality.
|
||||
def send_group(self, group_name, data):
|
||||
"""Update all feeds in a group with one call. Group_name should be the
|
||||
name of a group to update. Data should be a dict with an item for each
|
||||
feed in the group, where the key is the feed name and value is the new
|
||||
data row value. For example a group 'TestGroup' with feeds 'FeedOne'
|
||||
and 'FeedTwo' could be updated by calling:
|
||||
|
||||
send_group('TestGroup', {'FeedOne': 'value1', 'FeedTwo': 10})
|
||||
|
||||
This would add the value 'value1' to the feed 'FeedOne' and add the
|
||||
value 10 to the feed 'FeedTwo'.
|
||||
|
||||
def create_data(self, feed, data):
|
||||
"""Create a new row of data in the specified feed. Feed can be a feed ID,
|
||||
feed key, or feed name. Data must be an instance of the Data class with at
|
||||
least a value property set on it. Returns a Data instance with details
|
||||
about the newly appended row of data.
|
||||
"""
|
||||
path = "api/feeds/{}/data".format(feed)
|
||||
return Data.from_response(self._post(path, data._asdict()))
|
||||
After a successful update an instance of Group will be returned with
|
||||
metadata about the updated group.
|
||||
"""
|
||||
path = "api/groups/{0}/send".format(group_name)
|
||||
return Group.from_dict(self._post(path, {'value': data}))
|
||||
|
||||
#group functionality
|
||||
def send_group(self, group_name, data):
|
||||
group_name = quote(group_name)
|
||||
path = "api/groups/{}/send".format(group_name)
|
||||
return self._post(path, {'value': data})
|
||||
def receive_group(self, group):
|
||||
"""Retrieve the most recent value for the specified group. Group can be
|
||||
a group ID, group key, or group name. Returns a Group instance whose
|
||||
feeds property holds an array of Feed instances associated with the group.
|
||||
"""
|
||||
path = "api/groups/{0}/last".format(group)
|
||||
return Group.from_dict(self._get(path))
|
||||
|
||||
def receive_group(self, group_name):
|
||||
group_name = quote(group_name)
|
||||
path = "api/groups/{}/last".format(group_name)
|
||||
return self._get(path)
|
||||
def receive_next_group(self, group):
|
||||
"""Retrieve the next unread value from the specified group. Group can
|
||||
be a group ID, group key, or group name. Returns a Group instance whose
|
||||
feeds property holds an array of Feed instances associated with the
|
||||
group.
|
||||
"""
|
||||
path = "api/groups/{0}/next".format(group)
|
||||
return Group.from_dict(self._get(path))
|
||||
|
||||
def receive_next_group(self, group_name):
|
||||
group_name = quote(group_name)
|
||||
path = "api/groups/{}/next".format(group_name)
|
||||
return self._get(path)
|
||||
def receive_previous_group(self, group):
|
||||
"""Retrieve the previous unread value from the specified group. Group
|
||||
can be a group ID, group key, or group name. Returns a Group instance
|
||||
whose feeds property holds an array of Feed instances associated with
|
||||
the group.
|
||||
"""
|
||||
path = "api/groups/{0}/previous".format(group)
|
||||
return Group.from_dict(self._get(path))
|
||||
|
||||
def receive_previous_group(self, group_name):
|
||||
group_name = quote(group_name)
|
||||
path = "api/groups/{}/last".format(group_name)
|
||||
return self._get(path)
|
||||
def groups(self, group=None):
|
||||
"""Retrieve a list of all groups, or the specified group. If group is
|
||||
not specified a list of all groups will be returned. If group is
|
||||
specified it can be a group name, key, or ID and the requested group
|
||||
will be returned.
|
||||
"""
|
||||
if group is None:
|
||||
path = "api/groups/"
|
||||
return list(map(Group.from_dict, self._get(path)))
|
||||
else:
|
||||
path = "api/groups/{0}".format(group)
|
||||
return Group.from_dict(self._get(path))
|
||||
|
||||
def groups(self, group_id_or_key):
|
||||
path = "api/groups/{}".format(group_id_or_key)
|
||||
return self._get(path)
|
||||
def create_group(self, group):
|
||||
"""Create the specified group. Group should be an instance of the Group
|
||||
type with at least the name and feeds property set.
|
||||
"""
|
||||
path = "api/groups/"
|
||||
return Group.from_dict(self._post(path, group._asdict()))
|
||||
|
||||
def create_group(self, group_id_or_key, data):
|
||||
path = "api/groups/{}".format(group_id_or_key)
|
||||
return self._post(path, data)
|
||||
def delete_group(self, group):
|
||||
"""Delete the specified group. Group can be a group ID, group key, or
|
||||
group name.
|
||||
"""
|
||||
path = "api/groups/{0}".format(group)
|
||||
self._delete(path)
|
||||
|
|
|
|||
40
Adafruit_IO/errors.py
Normal file
40
Adafruit_IO/errors.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# Copyright (c) 2014 Adafruit Industries
|
||||
# Author: Tony DiCola
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
class AdafruitIOError(Exception):
|
||||
"""Base class for all Adafruit IO request failures."""
|
||||
pass
|
||||
|
||||
|
||||
class RequestError(Exception):
|
||||
"""General error for a failed Adafruit IO request."""
|
||||
def __init__(self, response):
|
||||
super(RequestError, self).__init__("Adafruit IO request failed: {0} {1}".format(
|
||||
response.status_code, response.reason))
|
||||
|
||||
|
||||
class ThrottlingError(AdafruitIOError):
|
||||
"""Too many requests have been made to Adafruit IO in a short period of time.
|
||||
Reduce the rate of requests and try again later.
|
||||
"""
|
||||
def __init__(self):
|
||||
super(ThrottlingError, self).__init__("Exceeded the limit of Adafruit IO " \
|
||||
"requests in a short period of time. Please reduce the rate of requests " \
|
||||
"and try again later.")
|
||||
117
Adafruit_IO/model.py
Normal file
117
Adafruit_IO/model.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
# Copyright (c) 2014 Adafruit Industries
|
||||
# Author: Tony DiCola
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
from collections import namedtuple
|
||||
# Handle python 2 and 3 (where map functions like itertools.imap)
|
||||
try:
|
||||
from itertools import imap as map
|
||||
except ImportError:
|
||||
# Ignore import error on python 3 since map already behaves as expected.
|
||||
pass
|
||||
|
||||
|
||||
# List of fields/properties that are present on a data object from IO.
|
||||
DATA_FIELDS = [ 'created_epoch',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'value',
|
||||
'completed_at',
|
||||
'feed_id',
|
||||
'expiration',
|
||||
'position',
|
||||
'id' ]
|
||||
|
||||
STREAM_FIELDS = [ 'completed_at',
|
||||
'created_at',
|
||||
'id',
|
||||
'value' ]
|
||||
|
||||
FEED_FIELDS = [ 'last_value_at',
|
||||
'name',
|
||||
'stream',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'unit_type',
|
||||
'mode',
|
||||
'key',
|
||||
'unit_symbol',
|
||||
'fixed',
|
||||
'last_value',
|
||||
'id' ]
|
||||
|
||||
GROUP_FIELDS = [ 'description',
|
||||
'source_keys',
|
||||
'id',
|
||||
'source',
|
||||
'key',
|
||||
'feeds',
|
||||
'properties',
|
||||
'name' ]
|
||||
|
||||
|
||||
# These are very simple data model classes that are based on namedtuple. This is
|
||||
# to keep the classes simple and prevent any confusion around updating data
|
||||
# locally and forgetting to send those updates back up to the IO service (since
|
||||
# tuples are immutable you can't change them!). Depending on how people use the
|
||||
# client it might be prudent to revisit this decision and consider making these
|
||||
# full fledged classes that are mutable.
|
||||
Data = namedtuple('Data', DATA_FIELDS)
|
||||
Stream = namedtuple('Stream', STREAM_FIELDS)
|
||||
Feed = namedtuple('Feed', FEED_FIELDS)
|
||||
Group = namedtuple('Group', GROUP_FIELDS)
|
||||
|
||||
|
||||
# Magic incantation to make all parameters to the initializers optional with a
|
||||
# default value of None.
|
||||
Data.__new__.__defaults__ = tuple(None for x in DATA_FIELDS)
|
||||
Stream.__new__.__defaults__ = tuple(None for x in STREAM_FIELDS)
|
||||
Feed.__new__.__defaults__ = tuple(None for x in FEED_FIELDS)
|
||||
Group.__new__.__defaults__ = tuple(None for x in GROUP_FIELDS)
|
||||
|
||||
|
||||
# Define methods to convert from dicts to the data types.
|
||||
def _from_dict(cls, data):
|
||||
# Convert dict to call to class initializer (to work with the data types
|
||||
# base on namedtuple). However be very careful to preserve forwards
|
||||
# compatibility by ignoring any attributes in the dict which are unknown
|
||||
# by the data type.
|
||||
params = {x: data.get(x, None) for x in cls._fields}
|
||||
return cls(**params)
|
||||
|
||||
|
||||
def _feed_from_dict(cls, data):
|
||||
params = {x: data.get(x, None) for x in cls._fields}
|
||||
# Parse the stream if provided and generate a stream instance.
|
||||
params['stream'] = Stream.from_dict(data.get('stream', {}))
|
||||
return cls(**params)
|
||||
|
||||
|
||||
def _group_from_dict(cls, data):
|
||||
params = {x: data.get(x, None) for x in cls._fields}
|
||||
# Parse the feeds if they're provided and generate feed instances.
|
||||
params['feeds'] = tuple(map(Feed.from_dict, data.get('feeds', [])))
|
||||
return cls(**params)
|
||||
|
||||
|
||||
# Now add the from_dict class methods defined above to the data types.
|
||||
Data.from_dict = classmethod(_from_dict)
|
||||
Stream.from_dict = classmethod(_from_dict)
|
||||
Feed.from_dict = classmethod(_feed_from_dict)
|
||||
Group.from_dict = classmethod(_group_from_dict)
|
||||
|
|
@ -1,18 +1,29 @@
|
|||
# MQTT-based client for Adafruit.IO
|
||||
# Author: Tony DiCola (tdicola@adafruit.com)
|
||||
#
|
||||
# Supports publishing and subscribing to feed changes from Adafruit IO using
|
||||
# the MQTT protcol.
|
||||
#
|
||||
# Depends on the following Python libraries:
|
||||
# - paho-mqtt: Paho MQTT client for python.
|
||||
# Copyright (c) 2014 Adafruit Industries
|
||||
# Author: Tony DiCola
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import logging
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
|
||||
SERVICE_HOST = 'io.adafruit.com'
|
||||
SERVICE_PORT = 1883
|
||||
# How long to wait before sending a keep alive (paho-mqtt configuration).
|
||||
KEEP_ALIVE_SEC = 3600 # One minute
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -23,12 +34,14 @@ class MQTTClient(object):
|
|||
using the MQTT protocol.
|
||||
"""
|
||||
|
||||
def __init__(self, key):
|
||||
def __init__(self, key, service_host='io.adafruit.com', service_port=1883):
|
||||
"""Create instance of MQTT client.
|
||||
|
||||
Required parameters:
|
||||
- key: The Adafruit.IO access key for your account.
|
||||
"""
|
||||
self._service_host = service_host
|
||||
self._service_port = service_port
|
||||
# Initialize event callbacks to be None so they don't fire.
|
||||
self.on_connect = None
|
||||
self.on_disconnect = None
|
||||
|
|
@ -69,11 +82,12 @@ class MQTTClient(object):
|
|||
def _mqtt_message(self, client, userdata, msg):
|
||||
logger.debug('Client on_message called.')
|
||||
# Parse out the feed id and call on_message callback.
|
||||
# Assumes topic looks like "api/feeds/{feed_id}/data/receive.json"
|
||||
# Assumes topic looks like "api/feeds/{feed}/data/receive.json"
|
||||
if self.on_message is not None and msg.topic.startswith('api/feeds/') \
|
||||
and len(msg.topic) >= 28:
|
||||
feed_id = msg.topic[10:-18]
|
||||
self.on_message(self, feed_id, msg.payload)
|
||||
feed = msg.topic[10:-18]
|
||||
payload = '' if msg.payload is None else msg.payload.decode('utf-8')
|
||||
self.on_message(self, feed, payload)
|
||||
|
||||
def connect(self, **kwargs):
|
||||
"""Connect to the Adafruit.IO service. Must be called before any loop
|
||||
|
|
@ -85,7 +99,7 @@ class MQTTClient(object):
|
|||
if self._connected:
|
||||
return
|
||||
# Connect to the Adafruit IO MQTT service.
|
||||
self._client.connect(SERVICE_HOST, port=SERVICE_PORT,
|
||||
self._client.connect(self._service_host, port=self._service_port,
|
||||
keepalive=KEEP_ALIVE_SEC, **kwargs)
|
||||
|
||||
def is_connected(self):
|
||||
|
|
@ -94,7 +108,7 @@ class MQTTClient(object):
|
|||
return self._connected
|
||||
|
||||
def disconnect(self):
|
||||
# Disconnect MQTT client if connected.
|
||||
"""Disconnect MQTT client if connected."""
|
||||
if self._connected:
|
||||
self._client.disconnect()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,12 @@
|
|||
0.9.0
|
||||
----
|
||||
Author: Tony DiCola
|
||||
- Added REST API support for all feed, data, group create/get/update/delete APIs.
|
||||
- Added explicit data model classes.
|
||||
- Added many integration tests to verify client & service.
|
||||
- Added docstrings to all public functions and classes.
|
||||
- Ported to work with Python 2 and 3.
|
||||
|
||||
0.0.1
|
||||
----
|
||||
Initial Changelog
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
Copyright (c) 2014 Adafruit
|
||||
Author: Justin Cooper
|
||||
Author: Justin Cooper and Tony DiCola
|
||||
|
||||
MIT License
|
||||
|
||||
|
|
|
|||
287
README.md
287
README.md
|
|
@ -1,11 +1,14 @@
|
|||
# adafruit-io
|
||||
|
||||
A [Python][1] client for use with with [io.adafruit.com][2].
|
||||
A [Python][1] client for use with with [io.adafruit.com][2]. Compatible with
|
||||
both Python 2 and Python 3.
|
||||
|
||||
## Installation
|
||||
|
||||
### Easy Installation
|
||||
|
||||
**NOTE: MODULE IS NOT YET ON PYPA SO THIS DOES NOT WORK YET. SKIP TO MANUAL INSTALL BELOW.**
|
||||
|
||||
If you have [pip installed](https://pip.pypa.io/en/latest/installing.html)
|
||||
(typically with ````apt-get install python-pip```` on a Debian/Ubuntu-based
|
||||
system) then run:
|
||||
|
|
@ -31,25 +34,24 @@ At a high level the Adafruit IO python client provides two interfaces to the
|
|||
service:
|
||||
|
||||
* A thin wrapper around the REST-based API. This is good for simple request and
|
||||
response applications.
|
||||
response applications like logging data.
|
||||
|
||||
* An MQTT client (based on [paho-mqtt](https://pypi.python.org/pypi/paho-mqtt))
|
||||
* A MQTT client (based on [paho-mqtt](https://pypi.python.org/pypi/paho-mqtt))
|
||||
which can publish and subscribe to feeds so it is immediately alerted of changes.
|
||||
This is good for applications which need to know when something has changed as
|
||||
quickly as possible, but requires keeping a connection to the service open at
|
||||
all times.
|
||||
quickly as possible.
|
||||
|
||||
To use either interface you'll first need to import the python client by adding
|
||||
an import such as the following at the top of your program:
|
||||
|
||||
```python
|
||||
import Adafruit_IO
|
||||
from Adafruit_IO import *
|
||||
```
|
||||
|
||||
Then a REST API client can be created with code like:
|
||||
|
||||
```python
|
||||
aio = Adafruit_IO.Client('xxxxxxxxxxxx')
|
||||
aio = Client('xxxxxxxxxxxx')
|
||||
```
|
||||
|
||||
Where 'xxxxxxxxxxxx' is your Adafruit IO API key.
|
||||
|
|
@ -57,7 +59,7 @@ Where 'xxxxxxxxxxxx' is your Adafruit IO API key.
|
|||
Alternatively an MQTT client can be created with code like:
|
||||
|
||||
```python
|
||||
mqtt = Adafruit_IO.MQTTClient('xxxxxxxxxxxx')
|
||||
mqtt = MQTTClient('xxxxxxxxxxxx')
|
||||
```
|
||||
|
||||
Again where 'xxxxxxxxxxxx' is your Adafruit IO API key.
|
||||
|
|
@ -83,28 +85,44 @@ example uses the REST API.
|
|||
from Adafruit_IO import Client
|
||||
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||
|
||||
# Send a value to a feed called 'Feed Name'.
|
||||
# Data can be of any type, string, number, hash, json.
|
||||
aio.send('Feed Name', data)
|
||||
# Send the value 100 to a feed called 'Foo'.
|
||||
aio.send('Foo', 100)
|
||||
|
||||
# Retrieve the most recent value from the feed 'Feed Name'.
|
||||
# Notice the returned object has a property called value.
|
||||
data = aio.receive("Feed Name")
|
||||
# Retrieve the most recent value from the feed 'Foo'.
|
||||
# Access the value by reading the `value` property on the returned Data object.
|
||||
# Note that all values retrieved from IO are strings so you might need to convert
|
||||
# them to an int or numeric type if you expect a number.
|
||||
data = aio.receive('Foo')
|
||||
print('Received value: {0}'.format(data.value))
|
||||
```
|
||||
|
||||
If you want to be notified of feed changes immediately without polling, consider
|
||||
using the MQTT client. See the [examples\mqtt_client.py](https://github.com/adafruit/io-client-python/blob/master/examples/mqtt_client.py) for an example of using the MQTT client.
|
||||
|
||||
### More Information
|
||||
|
||||
See the details below for more information about using the Adafruit IO python
|
||||
client. You can also print out the documentation on the client and classes by
|
||||
running:
|
||||
|
||||
```
|
||||
pydoc Adafruit_IO.client
|
||||
pydoc Adafruit_IO.mqtt_client
|
||||
pydoc Adafruit_IO.model
|
||||
pydoc Adafruit_IO.errors
|
||||
|
||||
## Table of Contents
|
||||
|
||||
* [Feeds](#feeds)
|
||||
* [Creation, Retrieval, Updating](#feed-creation-retrieval-updating)
|
||||
* [Create](#feed-creation)
|
||||
* [Read](#feed-retrieval)
|
||||
* [Update](#feed-updating)
|
||||
* [Delete](#feed-deletion)
|
||||
* [Data](#data)
|
||||
* [Create](#data-creation)
|
||||
* [Read](#data-retrieval)
|
||||
* [Updating, Deletion](#data-updating-deletion)
|
||||
* [Update](#data-updating)
|
||||
* [Delete](#data-deletion)
|
||||
* [Helper Methods](#helper-methods)
|
||||
* [Send](#send)
|
||||
* [Receive](#receive)
|
||||
|
|
@ -112,6 +130,10 @@ using the MQTT client. See the [examples\mqtt_client.py](https://github.com/ada
|
|||
* [Previous](#previous)
|
||||
* [Publishing and Subscribing](#publishing-and-subscribing)
|
||||
* [Groups](#groups)
|
||||
* [Create](#group-creation)
|
||||
* [Read](#group-retrieval)
|
||||
* [Update](#group-updating)
|
||||
* [Delete](#group-deletion)
|
||||
|
||||
### Feeds
|
||||
|
||||
|
|
@ -120,18 +142,76 @@ that gets pushed, and you will have one feed for each type of data you send to
|
|||
the system. You can have separate feeds for each sensor in a project, or you can
|
||||
use one feed to contain JSON encoded data for all of your sensors.
|
||||
|
||||
#### Feed Creation, Retrieval, Updating
|
||||
#### Feed Creation
|
||||
|
||||
TODO: The python client does not currently support creating, retrieving, or
|
||||
updating a feed's metadata. See the send helper function below for creating a
|
||||
feed and sending it new data.
|
||||
Create a feed by constructing a Feed instance with at least a name specified, and
|
||||
then pass it to the `create_feed(feed)` function:
|
||||
|
||||
```python
|
||||
# Import library and create instance of REST client.
|
||||
from Adafruit_IO import Client, Feed
|
||||
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||
|
||||
# Create Feed object with name 'Foo'.
|
||||
feed = Feed(name='Foo')
|
||||
|
||||
# Send the Feed to IO to create.
|
||||
# The returned object will contain all the details about the created feed.
|
||||
result = aio.create_feed(feed)
|
||||
```
|
||||
|
||||
Note that you can use the [send](#send) function to create a feed and send it a
|
||||
new value in a single call. It's recommended that you use send instead of
|
||||
manually constructing feed instances.
|
||||
|
||||
#### Feed Retrieval
|
||||
|
||||
You can get a list of your feeds by using the `feeds()` method which will return
|
||||
a list of Feed instances:
|
||||
|
||||
```python
|
||||
# Import library and create instance of REST client.
|
||||
from Adafruit_IO import Client
|
||||
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||
|
||||
# Get list of feeds.
|
||||
feeds = aio.feeds()
|
||||
|
||||
# Print out the feed names:
|
||||
for f in feeds:
|
||||
print('Feed: {0}'.format(f.name))
|
||||
```
|
||||
|
||||
Alternatively you can retrieve the metadata for a single feed by calling
|
||||
`feeds(feed)` and passing the name, ID, or key of a feed to retrieve:
|
||||
|
||||
```python
|
||||
# Import library and create instance of REST client.
|
||||
from Adafruit_IO import Client
|
||||
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||
|
||||
# Get feed 'Foo'
|
||||
feed = aio.feeds('Foo')
|
||||
|
||||
# Print out the feed metadata.
|
||||
print(feed)
|
||||
```
|
||||
|
||||
#### Feed Updating
|
||||
|
||||
TODO: This is not tested in the python client yet, but calling create_feed with
|
||||
a Feed instance should update the feed.
|
||||
|
||||
#### Feed Deletion
|
||||
|
||||
You can delete a feed by ID, key, or name by calling `aio.delete_feed(feed)`.
|
||||
You can delete a feed by ID, key, or name by calling `delete_feed(feed)`.
|
||||
ALL data in the feed will be deleted after calling this API!
|
||||
|
||||
```python
|
||||
# Import library and create instance of REST client.
|
||||
from Adafruit_IO import Client
|
||||
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||
|
||||
# Delete the feed with name 'Test'.
|
||||
aio.delete_feed('Test')
|
||||
```
|
||||
|
|
@ -145,47 +225,76 @@ and selecting certain pieces of data.
|
|||
#### Data Creation
|
||||
|
||||
Data can be created [after you create a feed](#data-creation), by using the
|
||||
`aio.create_data(feed, data)` method and passing it a new Data instance with
|
||||
a value set. See the [send function](#send) for a simpler and recommended way
|
||||
of adding a new value to a feed.
|
||||
`create_data(feed, data)` method and passing it a new Data instance a value.
|
||||
See also the [send function](#send) for a simpler way to add a value to feed and
|
||||
create the feed in one call.
|
||||
|
||||
```python
|
||||
# Import library and create instance of REST client.
|
||||
from Adafruit_IO import Client, Data
|
||||
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||
|
||||
# Create a data item with value 10 in the 'Test' feed.
|
||||
data = Adafruit_IO.Data(value=10)
|
||||
aio.create_stream('Test', data)
|
||||
data = Data(value=10)
|
||||
aio.create_data('Test', data)
|
||||
```
|
||||
|
||||
#### Data Retrieval
|
||||
|
||||
You can get all of the data for a feed by using the `aio.data(feed)` method. The
|
||||
You can get all of the data for a feed by using the `data(feed)` method. The
|
||||
result will be an array of all feed data, each returned as an instance of the
|
||||
Data class. Use the value property on each Data instance to get the data value.
|
||||
Data class. Use the value property on each Data instance to get the data value,
|
||||
and remember values are always returned as strings (so you might need to convert
|
||||
to an int or number if you expect a numeric value).
|
||||
|
||||
```python
|
||||
# Import library and create instance of REST client.
|
||||
from Adafruit_IO import Client
|
||||
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||
|
||||
# Get an array of all data from feed 'Test'
|
||||
data = aio.streams('Test')
|
||||
data = aio.data('Test')
|
||||
|
||||
# Print out all the results.
|
||||
for d in data:
|
||||
print('Data value: {0}'.format(d.value))
|
||||
```
|
||||
|
||||
You can also get a specific value by ID by using the `aio.feeds(feed, data_id)`
|
||||
You can also get a specific value by ID by using the `feeds(feed, data_id)`
|
||||
method. This will return a single piece of feed data with the provided data ID
|
||||
if it exists in the feed. The returned object will be an instance of the Data
|
||||
class.
|
||||
|
||||
```python
|
||||
# Import library and create instance of REST client.
|
||||
from Adafruit_IO import Client
|
||||
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||
|
||||
# Get a specific value by id.
|
||||
# This example assumes 1 is a valid data ID in the 'Test' feed
|
||||
data = aio.feeds('Test', 1)
|
||||
|
||||
# Print the value.
|
||||
print('Data value: {0}'.format(data.value))
|
||||
```
|
||||
|
||||
#### Data Updating, Deletion
|
||||
#### Data Updating
|
||||
|
||||
TODO: The python client does not currently support updating or deleting feed
|
||||
data.
|
||||
TODO: This is not tested in the python client, but calling create_data with a
|
||||
Data instance should update it.
|
||||
|
||||
#### Data Deletion
|
||||
|
||||
Values can be deleted by using the `delete(feed, data_id)` method:
|
||||
|
||||
```python
|
||||
# Import library and create instance of REST client.
|
||||
from Adafruit_IO import Client
|
||||
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||
|
||||
# Delete a data value from feed 'Test' with ID 1.
|
||||
data = aio.delete('Test', 1)
|
||||
```
|
||||
|
||||
#### Helper Methods
|
||||
|
||||
|
|
@ -193,46 +302,72 @@ There are a few helper methods that can make interacting with data a bit easier.
|
|||
|
||||
##### Send
|
||||
|
||||
You can use the `aio.send(feed_name, value)` method to append a new value to a
|
||||
You can use the `send(feed_name, value)` method to append a new value to a
|
||||
feed in one call. If the specified feed does not exist it will automatically be
|
||||
created. This is the recommended way to send data to Adafruit IO from the Python
|
||||
client.
|
||||
REST client.
|
||||
|
||||
```python
|
||||
# Add the value 98.6 to the feed 'Test Send Data'.
|
||||
aio.send('Test Send Data', 98.6)
|
||||
# Import library and create instance of REST client.
|
||||
from Adafruit_IO import Client
|
||||
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||
|
||||
# Add the value 98.6 to the feed 'Temperature'.
|
||||
aio.send('Temperature', 98.6)
|
||||
```
|
||||
|
||||
##### Receive
|
||||
|
||||
You can get the last inserted value by using the `aio.receive(feed)` method.
|
||||
You can get the last inserted value by using the `receive(feed)` method.
|
||||
|
||||
```python
|
||||
# Import library and create instance of REST client.
|
||||
from Adafruit_IO import Client
|
||||
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||
|
||||
# Get the last value of the temperature feed.
|
||||
data = aio.receive('Test')
|
||||
# Print the value.
|
||||
print('Data value: {0}'.format(data))
|
||||
|
||||
# Print the value and a message if it's over 100. Notice that the value is
|
||||
# converted from string to int because it always comes back as a string from IO.
|
||||
temp = int(data.value)
|
||||
print('Temperature: {0}'.format(temp))
|
||||
if temp > 100:
|
||||
print 'Hot enough for you?'
|
||||
```
|
||||
|
||||
##### Next
|
||||
|
||||
You can get the first inserted value that has not been processed by using the
|
||||
`aio.receive_next(feed)` method.
|
||||
You can get the first inserted value that has not been processed (read) by using
|
||||
the `receive_next(feed)` method.
|
||||
|
||||
```python
|
||||
# Import library and create instance of REST client.
|
||||
from Adafruit_IO import Client
|
||||
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||
|
||||
# Get next unread value from feed 'Test'.
|
||||
data = aio.receive_next('Test')
|
||||
|
||||
# Print the value.
|
||||
print('Data value: {0}'.format(data))
|
||||
print('Data value: {0}'.format(data.value))
|
||||
```
|
||||
|
||||
##### Previous
|
||||
|
||||
You can get the the last record that has been processed by using the
|
||||
`aio.receive_previous(feed)` method.
|
||||
You can get the the last record that has been processed (read) by using the
|
||||
`receive_previous(feed)` method.
|
||||
|
||||
```python
|
||||
# Import library and create instance of REST client.
|
||||
from Adafruit_IO import Client
|
||||
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||
|
||||
# Get previous read value from feed 'Test'.
|
||||
data = aio.receive_previous('Test')
|
||||
|
||||
# Print the value.
|
||||
print('Data value: {0}'.format(data))
|
||||
print('Data value: {0}'.format(data.value))
|
||||
```
|
||||
|
||||
#### Publishing and Subscribing
|
||||
|
|
@ -240,14 +375,70 @@ print('Data value: {0}'.format(data))
|
|||
You can get a readable stream of live data from your feed using the included
|
||||
MQTT client class.
|
||||
|
||||
TBD: Document using the MQTT client.
|
||||
TBD: Document using the MQTT client. For now see the [examples\mqtt_client.py](https://github.com/adafruit/io-client-python/blob/master/examples/mqtt_client.py) example which is fully documented with comments.
|
||||
|
||||
### Groups
|
||||
|
||||
Groups allow you to update and retrieve multiple feeds with one request. You can
|
||||
add feeds to multiple groups.
|
||||
|
||||
TBD
|
||||
#### Group Creation
|
||||
|
||||
TBD: Currently group creation doesn't work with the APIs. Groups must be created
|
||||
in the UI.
|
||||
|
||||
#### Group Retrieval
|
||||
|
||||
You can get a list of your groups by using the `groups()` method. This will
|
||||
return a list of Group instances. Each Group instance has metadata about the
|
||||
group, including a `feeds` property which is a tuple of all feeds in the group.
|
||||
|
||||
```python
|
||||
# Import library and create instance of REST client.
|
||||
from Adafruit_IO import Client
|
||||
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||
|
||||
# Get list of groups.
|
||||
groups = aio.groups()
|
||||
|
||||
# Print the group names and number of feeds in the group.
|
||||
for g in groups:
|
||||
print('Group {0} has {1} feed(s).'.format(g.name, len(g.feeds)))
|
||||
```
|
||||
|
||||
You can also get a specific group by ID, key, or name by using the
|
||||
`groups(group)` method:
|
||||
|
||||
```python
|
||||
# Import library and create instance of REST client.
|
||||
from Adafruit_IO import Client
|
||||
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||
|
||||
# Get group called 'GroupTest'.
|
||||
group = aio.groups('GroupTest')
|
||||
|
||||
# Print the group name and number of feeds in the group.
|
||||
print('Group {0} has {1} feed(s).'.format(group.name, len(group.feeds)))
|
||||
```
|
||||
|
||||
#### Group Updating
|
||||
|
||||
TBD This is not tested in the python client yet, but calling create_group should
|
||||
update a group.
|
||||
|
||||
#### Group Deletion
|
||||
|
||||
You can delete a group by ID, key, or name by using the `delete_group(group)`
|
||||
method:
|
||||
|
||||
```python
|
||||
# Import library and create instance of REST client.
|
||||
from Adafruit_IO import Client
|
||||
aio = Client('YOUR ADAFRUIT IO KEY')
|
||||
|
||||
# Delete group called 'GroupTest'.
|
||||
aio.delete_group('GroupTest')
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
# Example of using the MQTT client class to subscribe to and publish feed values.
|
||||
# Author: Tony DiCola (tdicola@adafruit.com)
|
||||
# Author: Tony DiCola
|
||||
|
||||
# Import standard python modules.
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
|
||||
# Import Adafruit IO client.
|
||||
import Adafruit_IO
|
||||
# Import Adafruit IO MQTT client.
|
||||
from Adafruit_IO import MQTTClient
|
||||
|
||||
|
||||
# Set to your Adafruit IO key.
|
||||
|
|
@ -37,7 +37,7 @@ def message(client, feed_id, payload):
|
|||
|
||||
|
||||
# Create an MQTT client instance.
|
||||
client = Adafruit_IO.MQTTClient(ADAFRUIT_IO_KEY)
|
||||
client = MQTTClient(ADAFRUIT_IO_KEY)
|
||||
|
||||
# Setup the callback functions defined above.
|
||||
client.on_connect = connected
|
||||
|
|
|
|||
32
examples/simple.py
Normal file
32
examples/simple.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Simple example of sending and receiving values from Adafruit IO with the REST
|
||||
# API client.
|
||||
# Author: Tony DiCola
|
||||
|
||||
# Import Adafruit IO REST client.
|
||||
from Adafruit_IO import Client
|
||||
|
||||
# Set to your Adafruit IO key.
|
||||
ADAFRUIT_IO_KEY = 'YOUR ADAFRUIT IO KEY'
|
||||
|
||||
# Create an instance of the REST client.
|
||||
aio = Client(ADAFRUIT_IO_KEY)
|
||||
|
||||
# Send a value to the feed 'Test'. This will create the feed if it doesn't
|
||||
# exist already.
|
||||
aio.send('Test', 42)
|
||||
|
||||
# Send a string value 'bar' to the feed 'Foo', again creating it if it doesn't
|
||||
# exist already.
|
||||
aio.send('Foo', 'bar')
|
||||
|
||||
# Now read the most recent value from the feed 'Test'. Notice that it comes
|
||||
# back as a string and should be converted to an int if performing calculations
|
||||
# on it.
|
||||
data = aio.receive('Test')
|
||||
print('Retrieved value from Test has attributes: {0}'.format(data))
|
||||
print('Latest value from Test: {0}'.format(data.value))
|
||||
|
||||
# Finally read the most revent value from feed 'Foo'.
|
||||
data = aio.receive('Foo')
|
||||
print('Retrieved value from Test has attributes: {0}'.format(data))
|
||||
print('Latest value from Test: {0}'.format(data.value))
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
from Adafruit_IO import Client
|
||||
io = Client('e2b0fac48ae32f324df4aa05247c16e991494b08')
|
||||
|
||||
r = io.send("Test Python", 12)
|
||||
print r
|
||||
|
||||
r = io.receive("Test Python")
|
||||
print r
|
||||
|
||||
r = io.receive_next("Test Python")
|
||||
print r
|
||||
332
ez_setup.py
Normal file
332
ez_setup.py
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
#!/usr/bin/env python
|
||||
"""Bootstrap setuptools installation
|
||||
|
||||
To use setuptools in your package's setup.py, include this
|
||||
file in the same directory and add this to the top of your setup.py::
|
||||
|
||||
from ez_setup import use_setuptools
|
||||
use_setuptools()
|
||||
|
||||
To require a specific version of setuptools, set a download
|
||||
mirror, or use an alternate download directory, simply supply
|
||||
the appropriate options to ``use_setuptools()``.
|
||||
|
||||
This file can also be run as a script to install or upgrade setuptools.
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
import optparse
|
||||
import subprocess
|
||||
import platform
|
||||
import textwrap
|
||||
import contextlib
|
||||
|
||||
from distutils import log
|
||||
|
||||
try:
|
||||
from urllib.request import urlopen
|
||||
except ImportError:
|
||||
from urllib2 import urlopen
|
||||
|
||||
try:
|
||||
from site import USER_SITE
|
||||
except ImportError:
|
||||
USER_SITE = None
|
||||
|
||||
DEFAULT_VERSION = "4.0.1"
|
||||
DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/"
|
||||
|
||||
def _python_cmd(*args):
|
||||
"""
|
||||
Return True if the command succeeded.
|
||||
"""
|
||||
args = (sys.executable,) + args
|
||||
return subprocess.call(args) == 0
|
||||
|
||||
|
||||
def _install(archive_filename, install_args=()):
|
||||
with archive_context(archive_filename):
|
||||
# installing
|
||||
log.warn('Installing Setuptools')
|
||||
if not _python_cmd('setup.py', 'install', *install_args):
|
||||
log.warn('Something went wrong during the installation.')
|
||||
log.warn('See the error message above.')
|
||||
# exitcode will be 2
|
||||
return 2
|
||||
|
||||
|
||||
def _build_egg(egg, archive_filename, to_dir):
|
||||
with archive_context(archive_filename):
|
||||
# building an egg
|
||||
log.warn('Building a Setuptools egg in %s', to_dir)
|
||||
_python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
|
||||
# returning the result
|
||||
log.warn(egg)
|
||||
if not os.path.exists(egg):
|
||||
raise IOError('Could not build the egg.')
|
||||
|
||||
|
||||
class ContextualZipFile(zipfile.ZipFile):
|
||||
"""
|
||||
Supplement ZipFile class to support context manager for Python 2.6
|
||||
"""
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
self.close()
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""
|
||||
Construct a ZipFile or ContextualZipFile as appropriate
|
||||
"""
|
||||
if hasattr(zipfile.ZipFile, '__exit__'):
|
||||
return zipfile.ZipFile(*args, **kwargs)
|
||||
return super(ContextualZipFile, cls).__new__(cls)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def archive_context(filename):
|
||||
# extracting the archive
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
log.warn('Extracting in %s', tmpdir)
|
||||
old_wd = os.getcwd()
|
||||
try:
|
||||
os.chdir(tmpdir)
|
||||
with ContextualZipFile(filename) as archive:
|
||||
archive.extractall()
|
||||
|
||||
# going in the directory
|
||||
subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
|
||||
os.chdir(subdir)
|
||||
log.warn('Now working in %s', subdir)
|
||||
yield
|
||||
|
||||
finally:
|
||||
os.chdir(old_wd)
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
|
||||
def _do_download(version, download_base, to_dir, download_delay):
|
||||
egg = os.path.join(to_dir, 'setuptools-%s-py%d.%d.egg'
|
||||
% (version, sys.version_info[0], sys.version_info[1]))
|
||||
if not os.path.exists(egg):
|
||||
archive = download_setuptools(version, download_base,
|
||||
to_dir, download_delay)
|
||||
_build_egg(egg, archive, to_dir)
|
||||
sys.path.insert(0, egg)
|
||||
|
||||
# Remove previously-imported pkg_resources if present (see
|
||||
# https://bitbucket.org/pypa/setuptools/pull-request/7/ for details).
|
||||
if 'pkg_resources' in sys.modules:
|
||||
del sys.modules['pkg_resources']
|
||||
|
||||
import setuptools
|
||||
setuptools.bootstrap_install_from = egg
|
||||
|
||||
|
||||
def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
|
||||
to_dir=os.curdir, download_delay=15):
|
||||
to_dir = os.path.abspath(to_dir)
|
||||
rep_modules = 'pkg_resources', 'setuptools'
|
||||
imported = set(sys.modules).intersection(rep_modules)
|
||||
try:
|
||||
import pkg_resources
|
||||
except ImportError:
|
||||
return _do_download(version, download_base, to_dir, download_delay)
|
||||
try:
|
||||
pkg_resources.require("setuptools>=" + version)
|
||||
return
|
||||
except pkg_resources.DistributionNotFound:
|
||||
return _do_download(version, download_base, to_dir, download_delay)
|
||||
except pkg_resources.VersionConflict as VC_err:
|
||||
if imported:
|
||||
msg = textwrap.dedent("""
|
||||
The required version of setuptools (>={version}) is not available,
|
||||
and can't be installed while this script is running. Please
|
||||
install a more recent version first, using
|
||||
'easy_install -U setuptools'.
|
||||
|
||||
(Currently using {VC_err.args[0]!r})
|
||||
""").format(VC_err=VC_err, version=version)
|
||||
sys.stderr.write(msg)
|
||||
sys.exit(2)
|
||||
|
||||
# otherwise, reload ok
|
||||
del pkg_resources, sys.modules['pkg_resources']
|
||||
return _do_download(version, download_base, to_dir, download_delay)
|
||||
|
||||
def _clean_check(cmd, target):
|
||||
"""
|
||||
Run the command to download target. If the command fails, clean up before
|
||||
re-raising the error.
|
||||
"""
|
||||
try:
|
||||
subprocess.check_call(cmd)
|
||||
except subprocess.CalledProcessError:
|
||||
if os.access(target, os.F_OK):
|
||||
os.unlink(target)
|
||||
raise
|
||||
|
||||
def download_file_powershell(url, target):
|
||||
"""
|
||||
Download the file at url to target using Powershell (which will validate
|
||||
trust). Raise an exception if the command cannot complete.
|
||||
"""
|
||||
target = os.path.abspath(target)
|
||||
ps_cmd = (
|
||||
"[System.Net.WebRequest]::DefaultWebProxy.Credentials = "
|
||||
"[System.Net.CredentialCache]::DefaultCredentials; "
|
||||
"(new-object System.Net.WebClient).DownloadFile(%(url)r, %(target)r)"
|
||||
% vars()
|
||||
)
|
||||
cmd = [
|
||||
'powershell',
|
||||
'-Command',
|
||||
ps_cmd,
|
||||
]
|
||||
_clean_check(cmd, target)
|
||||
|
||||
def has_powershell():
|
||||
if platform.system() != 'Windows':
|
||||
return False
|
||||
cmd = ['powershell', '-Command', 'echo test']
|
||||
with open(os.path.devnull, 'wb') as devnull:
|
||||
try:
|
||||
subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
|
||||
download_file_powershell.viable = has_powershell
|
||||
|
||||
def download_file_curl(url, target):
|
||||
cmd = ['curl', url, '--silent', '--output', target]
|
||||
_clean_check(cmd, target)
|
||||
|
||||
def has_curl():
|
||||
cmd = ['curl', '--version']
|
||||
with open(os.path.devnull, 'wb') as devnull:
|
||||
try:
|
||||
subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
|
||||
download_file_curl.viable = has_curl
|
||||
|
||||
def download_file_wget(url, target):
|
||||
cmd = ['wget', url, '--quiet', '--output-document', target]
|
||||
_clean_check(cmd, target)
|
||||
|
||||
def has_wget():
|
||||
cmd = ['wget', '--version']
|
||||
with open(os.path.devnull, 'wb') as devnull:
|
||||
try:
|
||||
subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
|
||||
download_file_wget.viable = has_wget
|
||||
|
||||
def download_file_insecure(url, target):
|
||||
"""
|
||||
Use Python to download the file, even though it cannot authenticate the
|
||||
connection.
|
||||
"""
|
||||
src = urlopen(url)
|
||||
try:
|
||||
# Read all the data in one block.
|
||||
data = src.read()
|
||||
finally:
|
||||
src.close()
|
||||
|
||||
# Write all the data in one block to avoid creating a partial file.
|
||||
with open(target, "wb") as dst:
|
||||
dst.write(data)
|
||||
|
||||
download_file_insecure.viable = lambda: True
|
||||
|
||||
def get_best_downloader():
|
||||
downloaders = (
|
||||
download_file_powershell,
|
||||
download_file_curl,
|
||||
download_file_wget,
|
||||
download_file_insecure,
|
||||
)
|
||||
viable_downloaders = (dl for dl in downloaders if dl.viable())
|
||||
return next(viable_downloaders, None)
|
||||
|
||||
def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
|
||||
to_dir=os.curdir, delay=15, downloader_factory=get_best_downloader):
|
||||
"""
|
||||
Download setuptools from a specified location and return its filename
|
||||
|
||||
`version` should be a valid setuptools version number that is available
|
||||
as an egg for download under the `download_base` URL (which should end
|
||||
with a '/'). `to_dir` is the directory where the egg will be downloaded.
|
||||
`delay` is the number of seconds to pause before an actual download
|
||||
attempt.
|
||||
|
||||
``downloader_factory`` should be a function taking no arguments and
|
||||
returning a function for downloading a URL to a target.
|
||||
"""
|
||||
# making sure we use the absolute path
|
||||
to_dir = os.path.abspath(to_dir)
|
||||
zip_name = "setuptools-%s.zip" % version
|
||||
url = download_base + zip_name
|
||||
saveto = os.path.join(to_dir, zip_name)
|
||||
if not os.path.exists(saveto): # Avoid repeated downloads
|
||||
log.warn("Downloading %s", url)
|
||||
downloader = downloader_factory()
|
||||
downloader(url, saveto)
|
||||
return os.path.realpath(saveto)
|
||||
|
||||
def _build_install_args(options):
|
||||
"""
|
||||
Build the arguments to 'python setup.py install' on the setuptools package
|
||||
"""
|
||||
return ['--user'] if options.user_install else []
|
||||
|
||||
def _parse_args():
|
||||
"""
|
||||
Parse the command line for options
|
||||
"""
|
||||
parser = optparse.OptionParser()
|
||||
parser.add_option(
|
||||
'--user', dest='user_install', action='store_true', default=False,
|
||||
help='install in user site package (requires Python 2.6 or later)')
|
||||
parser.add_option(
|
||||
'--download-base', dest='download_base', metavar="URL",
|
||||
default=DEFAULT_URL,
|
||||
help='alternative URL from where to download the setuptools package')
|
||||
parser.add_option(
|
||||
'--insecure', dest='downloader_factory', action='store_const',
|
||||
const=lambda: download_file_insecure, default=get_best_downloader,
|
||||
help='Use internal, non-validating downloader'
|
||||
)
|
||||
parser.add_option(
|
||||
'--version', help="Specify which version to download",
|
||||
default=DEFAULT_VERSION,
|
||||
)
|
||||
options, args = parser.parse_args()
|
||||
# positional arguments are ignored
|
||||
return options
|
||||
|
||||
def main():
|
||||
"""Install or upgrade setuptools and EasyInstall"""
|
||||
options = _parse_args()
|
||||
archive = download_setuptools(
|
||||
version=options.version,
|
||||
download_base=options.download_base,
|
||||
downloader_factory=options.downloader_factory,
|
||||
)
|
||||
return _install(archive, _build_install_args(options))
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
7
setup.py
7
setup.py
|
|
@ -1,8 +1,10 @@
|
|||
from ez_setup import use_setuptools
|
||||
use_setuptools()
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='adafruit-io',
|
||||
version='0.0.1',
|
||||
version='0.9.0',
|
||||
author='Justin Cooper',
|
||||
author_email='justin@adafruit.com',
|
||||
packages=['Adafruit_IO'],
|
||||
|
|
@ -11,6 +13,7 @@ setup(
|
|||
description='IO Client library for io.adafruit.com',
|
||||
long_description=open('README.md').read(),
|
||||
install_requires=[
|
||||
"apiclient >= 1.0.2"
|
||||
"requests",
|
||||
"paho-mqtt"
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,25 @@
|
|||
# Base testcase class with functions and state available to all tests.
|
||||
# Author: Tony DiCola (tdicola@adafruit.com)
|
||||
import os
|
||||
import time
|
||||
import unittest
|
||||
# Copyright (c) 2014 Adafruit Industries
|
||||
# Author: Tony DiCola
|
||||
|
||||
import Adafruit_IO
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import os
|
||||
import unittest
|
||||
|
||||
|
||||
class IOTestCase(unittest.TestCase):
|
||||
|
|
@ -18,13 +33,3 @@ class IOTestCase(unittest.TestCase):
|
|||
raise RuntimeError("ADAFRUIT_IO_KEY environment variable must be " \
|
||||
"set with valid Adafruit IO key to run this test!")
|
||||
return key
|
||||
|
||||
def ensure_feed_deleted(self, client, feed):
|
||||
"""Delete the provided feed if it exists. Does nothing if the feed
|
||||
doesn't exist.
|
||||
"""
|
||||
try:
|
||||
client.delete_feed(feed)
|
||||
except Adafruit_IO.RequestError:
|
||||
# Swallow the error if the feed doesn't exist.
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -1,13 +1,22 @@
|
|||
# Test REST client.
|
||||
# Author: Tony DiCola (tdicola@adafruit.com)
|
||||
import time
|
||||
import unittest
|
||||
|
||||
from Adafruit_IO import Client, Data, Feed, Group, RequestError
|
||||
|
||||
import Adafruit_IO
|
||||
import base
|
||||
|
||||
# Turn this on to see responses of requests from urllib.
|
||||
#import logging
|
||||
#logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
# Default config for tests to run against real Adafruit IO service with no proxy.
|
||||
BASE_URL = 'https://io.adafruit.com/'
|
||||
PROXIES = None
|
||||
|
||||
# Config to run tests against real Adafruit IO service over non-SSL and with a
|
||||
# a proxy running on localhost 8888 (good for getting traces with fiddler).
|
||||
#BASE_URL = 'http://io.adafruit.com/'
|
||||
#PROXIES = {'http': 'http://localhost:8888/'}
|
||||
|
||||
|
||||
class TestClient(base.IOTestCase):
|
||||
|
||||
|
|
@ -16,24 +25,39 @@ class TestClient(base.IOTestCase):
|
|||
#def tearDown(self):
|
||||
# time.sleep(30.0)
|
||||
|
||||
def get_client(self):
|
||||
# Construct an Adafruit IO REST client and return it.
|
||||
return Client(self.get_test_key(), proxies=PROXIES, base_url=BASE_URL)
|
||||
|
||||
def ensure_feed_deleted(self, client, feed):
|
||||
# Delete the specified feed if it exists.
|
||||
try:
|
||||
client.delete_feed(feed)
|
||||
except RequestError:
|
||||
# Swallow the error if the feed doesn't exist.
|
||||
pass
|
||||
|
||||
def ensure_group_deleted(self, client, group):
|
||||
# Delete the specified group if it exists.
|
||||
try:
|
||||
client.delete_group(group)
|
||||
except RequestError:
|
||||
# Swallow the error if the group doesn't exist.
|
||||
pass
|
||||
|
||||
def empty_feed(self, client, feed):
|
||||
# Remove all the data from a specified feed (but don't delete the feed).
|
||||
data = client.data(feed)
|
||||
for d in data:
|
||||
client.delete(feed, d.id)
|
||||
|
||||
def test_set_key(self):
|
||||
key = "unique_key_id"
|
||||
io = Adafruit_IO.Client(key)
|
||||
io = Client(key)
|
||||
self.assertEqual(key, io.key)
|
||||
|
||||
def test_delete_feed(self):
|
||||
io = Adafruit_IO.Client(self.get_test_key())
|
||||
io.send('TestFeed', 'foo') # Make sure a feed called TestFeed exists.
|
||||
io.delete_feed('TestFeed')
|
||||
self.assertRaises(Adafruit_IO.RequestError, io.receive, 'TestFeed')
|
||||
|
||||
def test_delete_nonexistant_feed_fails(self):
|
||||
io = Adafruit_IO.Client(self.get_test_key())
|
||||
self.ensure_feed_deleted(io, 'TestFeed')
|
||||
self.assertRaises(Adafruit_IO.RequestError, io.delete_feed, 'TestFeed')
|
||||
|
||||
def test_send_and_receive(self):
|
||||
io = Adafruit_IO.Client(self.get_test_key())
|
||||
io = self.get_client()
|
||||
self.ensure_feed_deleted(io, 'TestFeed')
|
||||
response = io.send('TestFeed', 'foo')
|
||||
self.assertEqual(response.value, 'foo')
|
||||
|
|
@ -41,22 +65,28 @@ class TestClient(base.IOTestCase):
|
|||
self.assertEqual(data.value, 'foo')
|
||||
|
||||
def test_receive_next(self):
|
||||
io = Adafruit_IO.Client(self.get_test_key())
|
||||
io = self.get_client()
|
||||
self.ensure_feed_deleted(io, 'TestFeed')
|
||||
io.send('TestFeed', 1)
|
||||
data = io.receive_next('TestFeed')
|
||||
self.assertEqual(int(data.value), 1)
|
||||
|
||||
# BUG: Previous jumps too far back: https://github.com/adafruit/io/issues/55
|
||||
@unittest.expectedFailure
|
||||
def test_receive_previous(self):
|
||||
io = Adafruit_IO.Client(self.get_test_key())
|
||||
io = self.get_client()
|
||||
self.ensure_feed_deleted(io, 'TestFeed')
|
||||
io.send('TestFeed', 1)
|
||||
io.receive_next('TestFeed')
|
||||
io.send('TestFeed', 2)
|
||||
io.receive_next('TestFeed') # Receive 1
|
||||
io.receive_next('TestFeed') # Receive 2
|
||||
data = io.receive_previous('TestFeed')
|
||||
self.assertEqual(int(data.value), 2)
|
||||
data = io.receive_previous('TestFeed')
|
||||
self.assertEqual(int(data.value), 1)
|
||||
|
||||
def test_data_on_feed_returns_all_data(self):
|
||||
io = Adafruit_IO.Client(self.get_test_key())
|
||||
io = self.get_client()
|
||||
self.ensure_feed_deleted(io, 'TestFeed')
|
||||
io.send('TestFeed', 1)
|
||||
io.send('TestFeed', 2)
|
||||
|
|
@ -66,7 +96,7 @@ class TestClient(base.IOTestCase):
|
|||
self.assertEqual(int(result[1].value), 2)
|
||||
|
||||
def test_data_on_feed_and_data_id_returns_data(self):
|
||||
io = Adafruit_IO.Client(self.get_test_key())
|
||||
io = self.get_client()
|
||||
self.ensure_feed_deleted(io, 'TestFeed')
|
||||
data = io.send('TestFeed', 1)
|
||||
result = io.data('TestFeed', data.id)
|
||||
|
|
@ -74,9 +104,135 @@ class TestClient(base.IOTestCase):
|
|||
self.assertEqual(int(data.value), int(result.value))
|
||||
|
||||
def test_create_data(self):
|
||||
io = Adafruit_IO.Client(self.get_test_key())
|
||||
io = self.get_client()
|
||||
self.ensure_feed_deleted(io, 'TestFeed')
|
||||
io.send('TestFeed', 1) # Make sure TestFeed exists.
|
||||
data = Adafruit_IO.Data(value=42)
|
||||
data = Data(value=42)
|
||||
result = io.create_data('TestFeed', data)
|
||||
self.assertEqual(int(result.value), 42)
|
||||
|
||||
def test_create_feed(self):
|
||||
io = self.get_client()
|
||||
self.ensure_feed_deleted(io, 'TestFeed')
|
||||
feed = Feed(name='TestFeed')
|
||||
result = io.create_feed(feed)
|
||||
self.assertEqual(result.name, 'TestFeed')
|
||||
|
||||
def test_feeds_returns_all_feeds(self):
|
||||
io = self.get_client()
|
||||
io.send('TestFeed', 1) # Make sure TestFeed exists.
|
||||
feeds = io.feeds()
|
||||
self.assertGreaterEqual(len(feeds), 1)
|
||||
names = set(map(lambda x: x.name, feeds))
|
||||
self.assertTrue('TestFeed' in names)
|
||||
|
||||
def test_feeds_returns_requested_feed(self):
|
||||
io = self.get_client()
|
||||
io.send('TestFeed', 1) # Make sure TestFeed exists.
|
||||
result = io.feeds('TestFeed')
|
||||
self.assertEqual(result.name, 'TestFeed')
|
||||
self.assertEqual(int(result.last_value), 1)
|
||||
|
||||
def test_delete_feed(self):
|
||||
io = self.get_client()
|
||||
io.send('TestFeed', 'foo') # Make sure a feed called TestFeed exists.
|
||||
io.delete_feed('TestFeed')
|
||||
self.assertRaises(RequestError, io.receive, 'TestFeed')
|
||||
|
||||
def test_delete_nonexistant_feed_fails(self):
|
||||
io = self.get_client()
|
||||
self.ensure_feed_deleted(io, 'TestFeed')
|
||||
self.assertRaises(RequestError, io.delete_feed, 'TestFeed')
|
||||
|
||||
# NOTE: The group tests require some manual setup once before they can run.
|
||||
# Log in to io.adafruit.com and create two feeds (called "GroupFeed1" and
|
||||
# "GroupFeed2" by default). Then add those feeds to a new Group called
|
||||
# "GroupTest". This only has to be done once for your account. Once group
|
||||
# creation is supported by the API this can be refactored to dynamically
|
||||
# create the group under test.
|
||||
def test_send_and_receive_group(self):
|
||||
io = self.get_client()
|
||||
self.empty_feed(io, 'GroupTest1')
|
||||
self.empty_feed(io, 'GroupTest2')
|
||||
response = io.send_group('GroupTest', {'GroupTest1': 1, 'GroupTest2': 2})
|
||||
self.assertEqual(len(response.feeds), 2)
|
||||
self.assertSetEqual(set(map(lambda x: int(x.stream.value), response.feeds)),
|
||||
set([1, 2])) # Compare sets because order might differ.
|
||||
#data = io.receive_group('GroupTest') # Group get by name currently broken.
|
||||
data = io.receive_group(response.id)
|
||||
self.assertEqual(len(data.feeds), 2)
|
||||
self.assertSetEqual(set(map(lambda x: int(x.stream.value), data.feeds)),
|
||||
set([1, 2]))
|
||||
|
||||
# BUG: Next doesn't return expected metadata: https://github.com/adafruit/io/issues/57
|
||||
@unittest.expectedFailure
|
||||
def test_receive_next_group(self):
|
||||
io = self.get_client()
|
||||
self.empty_feed(io, 'GroupTest1')
|
||||
self.empty_feed(io, 'GroupTest2')
|
||||
response = io.send_group('GroupTest', {'GroupTest1': 42, 'GroupTest2': 99})
|
||||
data = io.receive_next_group(response.id)
|
||||
self.assertEqual(len(data.feeds), 2)
|
||||
self.assertSetEqual(set(map(lambda x: int(x.stream.value), data.feeds)),
|
||||
set([42, 99]))
|
||||
|
||||
# BUG: Previous jumps too far back: https://github.com/adafruit/io/issues/55
|
||||
@unittest.expectedFailure
|
||||
def test_receive_previous_group(self):
|
||||
io = self.get_client()
|
||||
self.empty_feed(io, 'GroupTest1')
|
||||
self.empty_feed(io, 'GroupTest2')
|
||||
response = io.send_group('GroupTest', {'GroupTest1': 10, 'GroupTest2': 20})
|
||||
response = io.send_group('GroupTest', {'GroupTest1': 30, 'GroupTest2': 40})
|
||||
io.receive_next_group(response.id) # Receive 10, 20
|
||||
io.receive_next_group(response.id) # Receive 30, 40
|
||||
data = io.receive_previous_group(response.id)
|
||||
self.assertEqual(len(data.feeds), 2)
|
||||
self.assertSetEqual(set(map(lambda x: int(x.stream.value), data.feeds)),
|
||||
set([30, 40]))
|
||||
data = io.receive_previous_group(response.id)
|
||||
self.assertEqual(len(data.feeds), 2)
|
||||
self.assertSetEqual(set(map(lambda x: int(x.stream.value), data.feeds)),
|
||||
set([10, 20]))
|
||||
|
||||
def test_groups_returns_all_groups(self):
|
||||
io = self.get_client()
|
||||
groups = io.groups()
|
||||
self.assertGreaterEqual(len(groups), 1)
|
||||
names = set(map(lambda x: x.name, groups))
|
||||
self.assertTrue('GroupTest' in names)
|
||||
|
||||
def test_groups_retrieves_requested_group(self):
|
||||
io = self.get_client()
|
||||
response = io.groups('GroupTest')
|
||||
self.assertEqual(response.name, 'GroupTest')
|
||||
self.assertEqual(len(response.feeds), 2)
|
||||
|
||||
# BUG: Group create doesn't work: https://github.com/adafruit/io/issues/58
|
||||
@unittest.expectedFailure
|
||||
def test_create_group(self):
|
||||
io = self.get_client()
|
||||
self.ensure_group_deleted(io, 'GroupTest2')
|
||||
self.ensure_feed_deleted(io, 'GroupTest3')
|
||||
self.ensure_feed_deleted(io, 'GroupTest4')
|
||||
feed1 = io.create_feed(Feed(name='GroupTest3'))
|
||||
feed2 = io.create_feed(Feed(name='GroupTest4'))
|
||||
io.send('GroupTest3', 10)
|
||||
io.send('GroupTest4', 20)
|
||||
group = Group(name='GroupTest2', feeds=[feed1, feed2])
|
||||
response = io.create_group(group)
|
||||
self.assertEqual(response.name, 'GroupTest2')
|
||||
self.assertEqual(len(response.feeds), 2)
|
||||
|
||||
# BUG: Group create doesn't work: https://github.com/adafruit/io/issues/58
|
||||
@unittest.expectedFailure
|
||||
def test_delete_group(self):
|
||||
io = self.get_client()
|
||||
self.ensure_group_deleted(io, 'GroupDeleteTest')
|
||||
group = io.create_group(Group(name='GroupDeleteTest'))
|
||||
io.delete_group('GroupDeleteTest')
|
||||
self.assertRaises(RequestError, io.groups, 'GroupDeleteTest')
|
||||
|
||||
# TODO: Get by group name, key, and ID
|
||||
# TODO: Get data by name, key, ID
|
||||
# TODO: Tests around Adafruit IO keys (make multiple, test they work, etc.)
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
# Test Data instance from REST client.
|
||||
# Author: Tony DiCola (tdicola@adafruit.com)
|
||||
import Adafruit_IO
|
||||
import base
|
||||
|
||||
|
||||
class TestData(base.IOTestCase):
|
||||
|
||||
def test_data_properties_are_optional(self):
|
||||
data = Adafruit_IO.Data(value='foo', feed_id=10)
|
||||
self.assertEqual(data.value, 'foo')
|
||||
self.assertEqual(data.feed_id, 10)
|
||||
self.assertIsNone(data.created_epoch)
|
||||
self.assertIsNone(data.created_at)
|
||||
self.assertIsNone(data.updated_at)
|
||||
self.assertIsNone(data.completed_at)
|
||||
self.assertIsNone(data.expiration)
|
||||
self.assertIsNone(data.position)
|
||||
self.assertIsNone(data.id)
|
||||
|
||||
def test_from_response_ignores_unknown_items(self):
|
||||
data = Adafruit_IO.Data.from_response({'value': 'foo', 'feed_id': 10, 'unknown_param': 42})
|
||||
self.assertEqual(data.value, 'foo')
|
||||
self.assertEqual(data.feed_id, 10)
|
||||
self.assertIsNone(data.created_epoch)
|
||||
self.assertIsNone(data.created_at)
|
||||
self.assertIsNone(data.updated_at)
|
||||
self.assertIsNone(data.completed_at)
|
||||
self.assertIsNone(data.expiration)
|
||||
self.assertIsNone(data.position)
|
||||
self.assertIsNone(data.id)
|
||||
|
|
@ -1,23 +1,41 @@
|
|||
# Test error responses with REST client.
|
||||
# Author: Tony DiCola (tdicola@adafruit.com)
|
||||
# Copyright (c) 2014 Adafruit Industries
|
||||
# Author: Tony DiCola
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import time
|
||||
import unittest
|
||||
|
||||
import Adafruit_IO
|
||||
from Adafruit_IO import Client, RequestError, ThrottlingError
|
||||
import base
|
||||
|
||||
|
||||
class TestErrors(base.IOTestCase):
|
||||
|
||||
def test_request_error_from_bad_key(self):
|
||||
io = Adafruit_IO.Client("this is a bad key from a test")
|
||||
with self.assertRaises(Adafruit_IO.RequestError):
|
||||
io = Client("this is a bad key from a test")
|
||||
with self.assertRaises(RequestError):
|
||||
io.send("TestStream", 42)
|
||||
|
||||
@unittest.skip("Throttling test must be run in isolation to prevent other failures.")
|
||||
@unittest.skip("Throttling test must be run in isolation to prevent other tests from failing.")
|
||||
def test_throttling_error_after_6_requests_in_short_period(self):
|
||||
io = Adafruit_IO.Client(self.get_test_key())
|
||||
with self.assertRaises(Adafruit_IO.ThrottlingError):
|
||||
io = Client(self.get_test_key())
|
||||
with self.assertRaises(ThrottlingError):
|
||||
for i in range(6):
|
||||
io.send("TestStream", 42)
|
||||
time.sleep(0.1) # Small delay to keep from hammering network.
|
||||
|
|
|
|||
50
tests/test_model.py
Normal file
50
tests/test_model.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# Copyright (c) 2014 Adafruit Industries
|
||||
# Author: Tony DiCola
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
from Adafruit_IO import Data
|
||||
|
||||
import base
|
||||
|
||||
|
||||
class TestData(base.IOTestCase):
|
||||
|
||||
def test_data_properties_are_optional(self):
|
||||
data = Data(value='foo', feed_id=10)
|
||||
self.assertEqual(data.value, 'foo')
|
||||
self.assertEqual(data.feed_id, 10)
|
||||
self.assertIsNone(data.created_epoch)
|
||||
self.assertIsNone(data.created_at)
|
||||
self.assertIsNone(data.updated_at)
|
||||
self.assertIsNone(data.completed_at)
|
||||
self.assertIsNone(data.expiration)
|
||||
self.assertIsNone(data.position)
|
||||
self.assertIsNone(data.id)
|
||||
|
||||
def test_from_dict_ignores_unknown_items(self):
|
||||
data = Data.from_dict({'value': 'foo', 'feed_id': 10, 'unknown_param': 42})
|
||||
self.assertEqual(data.value, 'foo')
|
||||
self.assertEqual(data.feed_id, 10)
|
||||
self.assertIsNone(data.created_epoch)
|
||||
self.assertIsNone(data.created_at)
|
||||
self.assertIsNone(data.updated_at)
|
||||
self.assertIsNone(data.completed_at)
|
||||
self.assertIsNone(data.expiration)
|
||||
self.assertIsNone(data.position)
|
||||
self.assertIsNone(data.id)
|
||||
|
|
@ -1,9 +1,27 @@
|
|||
# Test MQTT client class.
|
||||
# Author: Tony DiCola (tdicola@adafruit.com)
|
||||
import logging
|
||||
# Copyright (c) 2014 Adafruit Industries
|
||||
# Author: Tony DiCola
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import time
|
||||
|
||||
import Adafruit_IO
|
||||
from Adafruit_IO import MQTTClient
|
||||
|
||||
import base
|
||||
|
||||
|
||||
|
|
@ -13,25 +31,27 @@ TIMEOUT_SEC = 5 # Max amount of time (in seconds) to wait for asyncronous event
|
|||
|
||||
class TestMQTTClient(base.IOTestCase):
|
||||
|
||||
def wait_until_connected(self, client, connect_value=True, timeout_sec=TIMEOUT_SEC):
|
||||
def wait_until_connected(self, client, connect_value=True,
|
||||
timeout_sec=TIMEOUT_SEC):
|
||||
# Pump the specified client message loop and wait until it's connected,
|
||||
# or the specified timeout has ellapsed. Can specify an explicit
|
||||
# connection state to wait for by setting connect_value (defaults to
|
||||
# waiting until connected, i.e. True).
|
||||
start = time.time()
|
||||
while client.is_connected() != connect_value and (time.time() - start) < timeout_sec:
|
||||
while client.is_connected() != connect_value and \
|
||||
(time.time() - start) < timeout_sec:
|
||||
client.loop()
|
||||
time.sleep(0)
|
||||
|
||||
def test_create_client(self):
|
||||
# Create MQTT test client.
|
||||
client = Adafruit_IO.MQTTClient(self.get_test_key())
|
||||
client = MQTTClient(self.get_test_key())
|
||||
# Verify not connected by default.
|
||||
self.assertFalse(client.is_connected())
|
||||
|
||||
def test_connect(self):
|
||||
# Create MQTT test client.
|
||||
client = Adafruit_IO.MQTTClient(self.get_test_key())
|
||||
client = MQTTClient(self.get_test_key())
|
||||
# Verify on_connect handler is called and expected client is provided.
|
||||
def on_connect(mqtt_client):
|
||||
self.assertEqual(mqtt_client, client)
|
||||
|
|
@ -44,7 +64,7 @@ class TestMQTTClient(base.IOTestCase):
|
|||
|
||||
def test_disconnect(self):
|
||||
# Create MQTT test client.
|
||||
client = Adafruit_IO.MQTTClient(self.get_test_key())
|
||||
client = MQTTClient(self.get_test_key())
|
||||
# Verify on_connect handler is called and expected client is provided.
|
||||
def on_disconnect(mqtt_client):
|
||||
self.assertEqual(mqtt_client, client)
|
||||
|
|
@ -60,12 +80,12 @@ class TestMQTTClient(base.IOTestCase):
|
|||
|
||||
def test_subscribe_and_publish(self):
|
||||
# Create MQTT test client.
|
||||
client = Adafruit_IO.MQTTClient(self.get_test_key())
|
||||
client = MQTTClient(self.get_test_key())
|
||||
# Save all on_message handler responses.
|
||||
messages = []
|
||||
def on_message(mqtt_client, feed_id, payload):
|
||||
def on_message(mqtt_client, feed, payload):
|
||||
self.assertEqual(mqtt_client, client)
|
||||
messages.append((feed_id, payload))
|
||||
messages.append((feed, payload))
|
||||
client.on_message = on_message
|
||||
# Connect and wait until on_connect event is fired.
|
||||
client.connect()
|
||||
|
|
|
|||
Loading…
Reference in a new issue