diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..5c410bd --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,46 @@ +name: Build-CI + +on: + push: + branches: + - master + +jobs: + + build: + runs-on: ubuntu-latest + environment: + name: IO + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + + - name: Install library + run: python3 setup.py install + + - name: Install sphinx + run: pip3 install pylint Sphinx sphinx-rtd-theme + + - name: Run unittests + env: + SECRET_IO_KEY: ${{ secrets.CI_IO_KEY }} + SECRET_IO_USER: ${{ secrets.CI_IO_USERNAME }} + run: | + echo "Secret key length: ${#SECRET_IO_KEY}" + echo "Secret username length: ${#SECRET_IO_USER}" + cd tests/ + ADAFRUIT_IO_KEY=$SECRET_IO_KEY ADAFRUIT_IO_USERNAME=$SECRET_IO_USER python -m unittest discover + cd .. + + - name: Generate documentation + run: | + cd docs && sphinx-build -E -W -b html . _build/html && cd .. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..27f5fee --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,33 @@ +name: Release CI + +on: + release: + types: [published] + +jobs: + upload-pypi: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Check For setup.py + id: need-pypi + run: | + echo ::set-output name=setup-py::$( find . -wholename './setup.py' ) + - name: Set up Python + if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') + uses: actions/setup-python@v1 + with: + python-version: '3.x' + - name: Install dependencies + if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + if: contains(steps.need-pypi.outputs.setup-py, 'setup.py') + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist + twine upload dist/* \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a6b8eba..0000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -language: python -dist: trusty -sudo: required -python: -- '3.6' -cache: - pip: true -install: -- python3 setup.py install -- pip3 install pylint Sphinx sphinx-rtd-theme -- pip3 install . -script: -- cd docs && sphinx-build -E -W -b html . _build/html && cd .. -- cd tests/ -- python3 -m unittest discover -- cd .. -deploy: - provider: pypi - user: adafruit-travis - password: - secure: B23uQWyBD9nE4zP+D8n3c8P9h2XsOdJWelVIwBoOL/E8LAqQ60xKRhYo+dbYFj/4L0xPsYxVoF8xdZCu+1rjXvFU6Eot9pgna7DBGzu8Qwzq7CS0Hq014+XhZR+36T9YAOl6Ehr7JR3XMCf51FGfq15myPvKFkCMjxpvxIlVsqg= - on: - tags: true diff --git a/Adafruit_IO/__init__.py b/Adafruit_IO/__init__.py index fedf663..e34eb3d 100644 --- a/Adafruit_IO/__init__.py +++ b/Adafruit_IO/__init__.py @@ -21,5 +21,5 @@ from .client import Client from .mqtt_client import MQTTClient from .errors import AdafruitIOError, RequestError, ThrottlingError, MQTTError -from .model import Data, Feed, Group -from ._version import __version__ \ No newline at end of file +from .model import Data, Feed, Group, Dashboard, Block, Layout +from ._version import __version__ diff --git a/Adafruit_IO/_version.py b/Adafruit_IO/_version.py index ff87090..892994a 100644 --- a/Adafruit_IO/_version.py +++ b/Adafruit_IO/_version.py @@ -1 +1 @@ -__version__ = "2.0.17" \ No newline at end of file +__version__ = "2.8.0" diff --git a/Adafruit_IO/client.py b/Adafruit_IO/client.py index e74ae10..3f92326 100644 --- a/Adafruit_IO/client.py +++ b/Adafruit_IO/client.py @@ -18,15 +18,22 @@ # 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 +from time import struct_time import json -import pkg_resources import platform +import pkg_resources +import re +from urllib.parse import urlparse +from urllib.parse import parse_qs # import logging import requests from .errors import RequestError, ThrottlingError -from .model import Data, Feed, Group +from .model import Data, Feed, Group, Dashboard, Block, Layout + +DEFAULT_PAGE_LIMIT = 100 # set outgoing version, pulled from setup.py version = pkg_resources.require("Adafruit_IO")[0].version @@ -42,32 +49,66 @@ class Client(object): REST API. Use this client class to send, receive, and enumerate feed data. """ - def __init__(self, username, key, proxies=None, base_url='https://io.adafruit.com', api_version = 'v2'): + def __init__(self, username, 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, a - base_url to point at a different Adafruit IO service (the default is - the production Adafruit IO service over SSL), and a api_version to - add support for future API versions. + provide a proxies dict in the format used by the requests library, + and a base_url to point at a different Adafruit IO service + (the default is the production Adafruit IO service over SSL). """ self.username = username self.key = key self.proxies = proxies - self.api_version = api_version - # self.logger = logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') + # self.logger = logging.basicConfig(level=logging.DEBUG, + # format='%(asctime)s - %(levelname)s - %(message)s') # 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, is_time=None): - if not is_time: - return '{0}/api/{1}/{2}/{3}'.format(self.base_url, self.api_version, self.username, path) - else: # return a call to https://io.adafruit.com/api/v2/time/{unit} - return '{0}/api/{1}/{2}'.format(self.base_url, self.api_version, path) + # Store the last response of a get or post + self._last_response = None + + @staticmethod + def to_red(data): + """Hex color feed to red channel. + + :param int data: Color value, in hexadecimal. + """ + return ((int(data[1], 16))*16) + int(data[2], 16) + + @staticmethod + def to_green(data): + """Hex color feed to green channel. + + :param int data: Color value, in hexadecimal. + """ + return (int(data[3], 16) * 16) + int(data[4], 16) + + @staticmethod + def to_blue(data): + """Hex color feed to blue channel. + + :param int data: Color value, in hexadecimal. + """ + return (int(data[5], 16) * 16) + int(data[6], 16) + + @staticmethod + def _headers(given): + headers = default_headers.copy() + headers.update(given) + return headers + @staticmethod + def _create_payload(value, metadata): + if metadata is not None: + payload = Data(value=value, lat=metadata['lat'], lon=metadata['lon'], + ele=metadata['ele'], created_at=metadata['created_at']) + return payload + return Data(value=value) - def _handle_error(self, response): + @staticmethod + def _handle_error(response): # Throttling Error if response.status_code == 429: raise ThrottlingError() @@ -79,20 +120,17 @@ def _handle_error(self, response): raise RequestError(response) # Else do nothing if there was no error. - def _headers(self, given): - headers = default_headers.copy() - headers.update(given) - return headers + def _compose_url(self, path): + return '{0}/api/{1}/{2}/{3}'.format(self.base_url, 'v2', self.username, path) - def _get(self, path, is_time=None): - response = requests.get(self._compose_url(path, is_time), + def _get(self, path, params=None): + response = requests.get(self._compose_url(path), headers=self._headers({'X-AIO-Key': self.key}), - proxies=self.proxies) + proxies=self.proxies, + params=params) + self._last_response = response self._handle_error(response) - if not is_time: - return response.json() - else: # time doesn't need to serialize into json, just return text - return response.text + return response.json() def _post(self, path, data): response = requests.post(self._compose_url(path), @@ -100,24 +138,37 @@ def _post(self, path, data): 'Content-Type': 'application/json'}), proxies=self.proxies, data=json.dumps(data)) + self._last_response = response self._handle_error(response) return response.json() def _delete(self, path): response = requests.delete(self._compose_url(path), headers=self._headers({'X-AIO-Key': self.key, - 'Content-Type': 'application/json'}), + 'Content-Type': 'application/json'}), proxies=self.proxies) + self._last_response = response self._handle_error(response) # Data functionality. - def send_data(self, feed, value): + def send_data(self, feed, value, metadata=None, precision=None): """Helper function to simplify adding a value to a feed. Will append the specified value to the feed identified by either name, key, or ID. Returns a Data instance with details about the newly appended row of data. Note that send_data now operates the same as append. + + :param string feed: Name/Key/ID of Adafruit IO feed. + :param string value: Value to send. + :param dict metadata: Optional metadata associated with the value. + :param int precision: Optional amount of precision points to send. """ - return self.create_data(feed, Data(value=value)) + if precision: + try: + value = round(value, precision) + except NotImplementedError: + raise NotImplementedError("Using the precision kwarg requires a float value") + payload = self._create_payload(value, metadata) + return self.create_data(feed, payload) send = send_data @@ -126,6 +177,9 @@ def send_batch_data(self, feed, data_list): 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. + + :param string feed: Name/Key/ID of Adafruit IO feed. + :param Data data_list: Multiple data values. """ path = "feeds/{0}/data/batch".format(feed) data_dict = type(data_list)((data._asdict() for data in data_list)) @@ -136,148 +190,293 @@ def append(self, feed, value): specified value to the feed identified by either name, key, or ID. Returns a Data instance with details about the newly appended row of data. Note that unlike send the feed should exist before calling append. + + :param string feed: Name/Key/ID of Adafruit IO feed. + :param string value: Value to append to feed. """ return self.create_data(feed, Data(value=value)) - def send_location_data(self, feed, value, lat, lon, ele): - """Sends locational data to a feed + def receive_time(self, timezone=None): + """Returns a struct_time from the Adafruit IO Server based on requested + timezone, or automatically based on the device's IP address. + https://docs.python.org/3.7/library/time.html#time.struct_time + + :param string timezone: Optional timezone to return the time in. + See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List + """ + path = 'integrations/time/struct.json' + if timezone: + path += f'?tz={timezone}' + return self._parse_time_struct(self._get(path)) + + @staticmethod + def _parse_time_struct(time_dict: dict) -> time.struct_time: + """Parse the time data returned by the server and return a time_struct + + Corrects for the weekday returned by the server in Sunday=0 format + (Python expects Monday=0) + """ + wday = (time_dict['wday'] - 1) % 7 + return struct_time((time_dict['year'], time_dict['mon'], time_dict['mday'], + time_dict['hour'], time_dict['min'], time_dict['sec'], + wday, time_dict['yday'], time_dict['isdst'])) + + def receive_weather(self, weather_id=None): + """Adafruit IO Weather Service, Powered by Dark Sky - args: - - lat: latitude - - lon: logitude - - ele: elevation - - (optional) value: value to send to the feed + :param int id: optional ID for retrieving a specified weather record. """ - return self.create_data(feed, Data(value = value,lat=lat, lon=lon, ele=ele)) + if weather_id: + weather_path = "integrations/weather/{0}".format(weather_id) + else: + weather_path = "integrations/weather" + return self._get(weather_path) - def receive_time(self, time): - """Returns the time from the Adafruit IO server. + def receive_random(self, randomizer_id=None): + """Access to Adafruit IO's Random Data + service. - args: - - time (string): millis, seconds, ISO-8601 + :param int randomizer_id: optional ID for retrieving a specified randomizer. """ - timepath = "time/{0}".format(time) - return self._get(timepath, is_time=True) + if randomizer_id: + random_path = "integrations/words/{0}".format(randomizer_id) + else: + random_path = "integrations/words" + return self._get(random_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. + """Retrieve the most recent value for the specified feed. Returns a Data + instance whose value property holds the retrieved value. + + :param string feed: Name/Key/ID of Adafruit IO feed. """ path = "feeds/{0}/data/last".format(feed) return Data.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. + """Retrieve the next unread value from the specified feed. Returns a Data + instance whose value property holds the retrieved value. + + :param string feed: Name/Key/ID of Adafruit IO feed. """ path = "feeds/{0}/data/next".format(feed) return Data.from_dict(self._get(path)) 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. + """Retrieve the previous unread value from the specified feed. Returns a + Data instance whose value property holds the retrieved value. + + :param string feed: Name/Key/ID of Adafruit IO feed. """ path = "feeds/{0}/data/previous".format(feed) return Data.from_dict(self._get(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. + def data(self, feed, data_id=None, max_results=DEFAULT_PAGE_LIMIT): + """Retrieve data from a feed. If data_id is not specified then all the data + for the feed will be returned in an array. + + :param string feed: Name/Key/ID of Adafruit IO feed. + :param string data_id: ID of the piece of data to delete. + :param int max_results: The maximum number of results to return. To + return all data, set to None. """ - if data_id is None: - path = "feeds/{0}/data".format(feed) - return list(map(Data.from_dict, self._get(path))) - else: + if max_results is None: + res = self._get(f'feeds/{feed}/details') + max_results = res['details']['data']['count'] + if data_id: path = "feeds/{0}/data/{1}".format(feed, data_id) return Data.from_dict(self._get(path)) + params = {'limit': max_results} if max_results else None + data = [] + path = "feeds/{0}/data".format(feed) + while len(data) < max_results: + data.extend(list(map(Data.from_dict, self._get(path, + params=params)))) + nlink = self.get_next_link() + if not nlink: + break + # Parse the link for the query parameters + params = parse_qs(urlparse(nlink).query) + if max_results: + params['limit'] = max_results - len(data) + return data + + def get_next_link(self): + """Parse the `next` page URL in the pagination Link header. + + This is necessary because of a bug in the API's implementation of the + link header. If that bug is fixed, the link would be accesible by + response.links['next']['url'] and this method would be broken. + + :return: The url for the next page of data + :rtype: str + """ + if not self._last_response: + return + link_header = self._last_response.headers['link'] + res = re.search('rel="next", <(.+?)>', link_header) + if not res: + return + return res.groups()[0] + 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. + """Create a new row of data in the specified feed. + Returns a Data instance with details about the newly + appended row of data. + + :param string feed: Name/Key/ID of Adafruit IO feed. + :param Data data: Instance of the Data class. Must have a value property set. """ path = "feeds/{0}/data".format(feed) return Data.from_dict(self._post(path, data._asdict())) 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. + """Delete data from a feed. + + :param string feed: Name/Key/ID of Adafruit IO feed. + :param string data_id: ID of the piece of data to delete. """ path = "feeds/{0}/data/{1}".format(feed, data_id) self._delete(path) - def toRed(self, data): - """Hex color feed to red channel. - """ - return ((int(data[1], 16))*16) + int(data[2], 16) - - def toGreen(self, data): - """Hex color feed to green channel. - """ - return (int(data[3], 16) * 16) + int(data[4], 16) - - def toBlue(self, data): - """Hex color feed to blue channel. - """ - return (int(data[5], 16) * 16) + int(data[6], 16) - - # Feed functionality. + # 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. + specified a list of all feeds will be returned. + + :param string feed: Name/Key/ID of Adafruit IO feed, defaults to None. """ if feed is None: path = "feeds" return list(map(Feed.from_dict, self._get(path))) - else: - path = "feeds/{0}".format(feed) - return Feed.from_dict(self._get(path)) + path = "feeds/{0}".format(feed) + return Feed.from_dict(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. + def create_feed(self, feed, group_key=None): + """Create the specified feed. + + :param string feed: Key of Adafruit IO feed. + :param group_key group: Group to place new feed in. """ + f = feed._asdict() + del f['id'] # Don't pass id on create call path = "feeds/" - return Feed.from_dict(self._post(path, {"feed": feed._asdict()})) + if group_key is not None: # create feed in a group + path="/groups/%s/feeds"%group_key + return Feed.from_dict(self._post(path, {"feed": f})) + return Feed.from_dict(self._post(path, {"feed": f})) def delete_feed(self, feed): - """Delete the specified feed. Feed can be a feed ID, feed key, or feed - name. + """Delete the specified feed. + + :param string feed: Name/Key/ID of Adafruit IO feed. """ path = "feeds/{0}".format(feed) self._delete(path) # Group functionality. 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. + """Retrieve a list of all groups, or the specified group. + + :param string group: Name/Key/ID of Adafruit IO Group. Defaults to None. """ if group is None: path = "groups/" return list(map(Group.from_dict, self._get(path))) - else: - path = "groups/{0}".format(group) - return Group.from_dict(self._get(path)) + path = "groups/{0}".format(group) + return Group.from_dict(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. + """Create the specified group. + + :param string group: Name/Key/ID of Adafruit IO Group. """ path = "groups/" return Group.from_dict(self._post(path, group._asdict())) def delete_group(self, group): - """Delete the specified group. Group can be a group ID, group key, or - group name. + """Delete the specified group. + + :param string group: Name/Key/ID of Adafruit IO Group. """ path = "groups/{0}".format(group) - self._delete(path) \ No newline at end of file + self._delete(path) + + # Dashboard functionality. + def dashboards(self, dashboard=None): + """Retrieve a list of all dashboards, or the specified dashboard. + + :param string dashboard: Key of Adafruit IO Dashboard. Defaults to None. + """ + if dashboard is None: + path = "dashboards/" + return list(map(Dashboard.from_dict, self._get(path))) + path = "dashboards/{0}".format(dashboard) + return Dashboard.from_dict(self._get(path)) + + def create_dashboard(self, dashboard): + """Create the specified dashboard. + + :param Dashboard dashboard: Dashboard object to create + """ + path = "dashboards/" + return Dashboard.from_dict(self._post(path, dashboard._asdict())) + + def delete_dashboard(self, dashboard): + """Delete the specified dashboard. + + :param string dashboard: Key of Adafruit IO Dashboard. + """ + path = "dashboards/{0}".format(dashboard) + self._delete(path) + + # Block functionality. + def blocks(self, dashboard, block=None): + """Retrieve a list of all blocks from a dashboard, or the specified block. + + :param string dashboard: Key of Adafruit IO Dashboard. + :param string block: id of Adafruit IO Block. Defaults to None. + """ + if block is None: + path = "dashboards/{0}/blocks".format(dashboard) + return list(map(Block.from_dict, self._get(path))) + path = "dashboards/{0}/blocks/{1}".format(dashboard, block) + return Block.from_dict(self._get(path)) + + def create_block(self, dashboard, block): + """Create the specified block under the specified dashboard. + + :param string dashboard: Key of Adafruit IO Dashboard. + :param Block block: Block object to create under dashboard + """ + path = "dashboards/{0}/blocks".format(dashboard) + return Block.from_dict(self._post(path, block._asdict())) + + def delete_block(self, dashboard, block): + """Delete the specified block. + + :param string dashboard: Key of Adafruit IO Dashboard. + :param string block: id of Adafruit IO Block. + """ + path = "dashboards/{0}/blocks/{1}".format(dashboard, block) + self._delete(path) + + # Layout functionality. + def layouts(self, dashboard): + """Retrieve the layouts array from a dashboard + + :param string dashboard: key of Adafruit IO Dashboard. + """ + path = "dashboards/{0}".format(dashboard) + dashboard = self._get(path) + return Layout.from_dict(dashboard['layouts']) + + def update_layout(self, dashboard, layout): + """Update the layout of the specified dashboard. + + :param string dashboard: Key of Adafruit IO Dashboard. + :param Layout layout: Layout object to update under dashboard + """ + path = "dashboards/{0}/update_layouts".format(dashboard) + return Layout.from_dict(self._post(path, {'layouts': layout._asdict()})) diff --git a/Adafruit_IO/errors.py b/Adafruit_IO/errors.py index ba6d88f..52b3fd7 100644 --- a/Adafruit_IO/errors.py +++ b/Adafruit_IO/errors.py @@ -20,14 +20,7 @@ # SOFTWARE. import json, requests - -# MQTT RC Error Types -MQTT_ERRORS = [ 'Connection successful', - 'Incorrect protocol version', - 'Invalid Client ID', - 'Server unavailable ', - 'Bad username or password', - 'Not authorized' ] +from paho.mqtt.client import error_string class AdafruitIOError(Exception): """Base class for all Adafruit IO request failures.""" @@ -63,6 +56,6 @@ class MQTTError(Exception): """Handles connection attempt failed errors. """ def __init__(self, response): - error = MQTT_ERRORS[response] + error = error_string(response) super(MQTTError, self).__init__(error) pass \ No newline at end of file diff --git a/Adafruit_IO/model.py b/Adafruit_IO/model.py index 04bb0c8..51d5633 100644 --- a/Adafruit_IO/model.py +++ b/Adafruit_IO/model.py @@ -43,6 +43,7 @@ FEED_FIELDS = [ 'name', 'key', + 'id', 'description', 'unit_type', 'unit_symbol', @@ -61,6 +62,26 @@ 'properties', 'name' ] +DASHBOARD_FIELDS = [ 'name', + 'key', + 'description', + 'show_header', + 'color_mode', + 'block_borders', + 'header_image_url', + 'blocks' ] + +BLOCK_FIELDS = [ 'name', + 'id', + 'visual_type', + 'properties', + 'block_feeds' ] + +LAYOUT_FIELDS = ['xl', + 'lg', + 'md', + 'sm', + 'xs' ] # 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 @@ -71,15 +92,24 @@ Data = namedtuple('Data', DATA_FIELDS) Feed = namedtuple('Feed', FEED_FIELDS) Group = namedtuple('Group', GROUP_FIELDS) - +Dashboard = namedtuple('Dashboard', DASHBOARD_FIELDS) +Block = namedtuple('Block', BLOCK_FIELDS) +Layout = namedtuple('Layout', LAYOUT_FIELDS) # Magic incantation to make all parameters to the initializers optional with a # default value of None. Group.__new__.__defaults__ = tuple(None for x in GROUP_FIELDS) Data.__new__.__defaults__ = tuple(None for x in DATA_FIELDS) +Layout.__new__.__defaults__ = tuple(None for x in LAYOUT_FIELDS) + +# explicitly set dashboard values so that 'color_mode' is 'dark' +Dashboard.__new__.__defaults__ = (None, None, None, False, "dark", True, None, None) + +# explicitly set block values so 'properties' is a dictionary +Block.__new__.__defaults__ = (None, None, None, {}, None) # explicitly set feed values -Feed.__new__.__defaults__ = (None, None, None, None, None, 'ON', 'Private', None, None, None) +Feed.__new__.__defaults__ = (None, None, None, None, None, None, 'ON', 'Private', None, None, None) # Define methods to convert from dicts to the data types. def _from_dict(cls, data): @@ -103,7 +133,17 @@ def _group_from_dict(cls, data): return cls(**params) +def _dashboard_from_dict(cls, data): + params = {x: data.get(x, None) for x in cls._fields} + # Parse the blocks if they're provided and generate block instances. + params['blocks'] = tuple(map(Block.from_dict, data.get('blocks', []))) + return cls(**params) + + # Now add the from_dict class methods defined above to the data types. Data.from_dict = classmethod(_from_dict) Feed.from_dict = classmethod(_feed_from_dict) Group.from_dict = classmethod(_group_from_dict) +Dashboard.from_dict = classmethod(_dashboard_from_dict) +Block.from_dict = classmethod(_from_dict) +Layout.from_dict = classmethod(_from_dict) diff --git a/Adafruit_IO/mqtt_client.py b/Adafruit_IO/mqtt_client.py index c032dae..198b4d6 100644 --- a/Adafruit_IO/mqtt_client.py +++ b/Adafruit_IO/mqtt_client.py @@ -1,5 +1,5 @@ -# Copyright (c) 2014 Adafruit Industries -# Author: Tony DiCola +# Copyright (c) 2020 Adafruit Industries +# Author: Tony DiCola, Brent Rubell # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -29,6 +29,11 @@ logger = logging.getLogger(__name__) +forecast_types = ["current", "forecast_minutes_5", + "forecast_minutes_30", "forecast_hours_1", + "forecast_hours_2", "forecast_hours_6", + "forecast_hours_24", "forecast_days_1", + "forecast_days_2", "forecast_days_5",] class MQTTClient(object): """Interface for publishing and subscribing to feed changes on Adafruit IO @@ -38,9 +43,10 @@ class MQTTClient(object): def __init__(self, username, key, service_host='io.adafruit.com', secure=True): """Create instance of MQTT client. - :param username: Adafruit.IO Username for your account. - :param key: Adafruit IO access key (AIO Key) for your account. - :param secure: (optional, boolean) Switches secure/insecure connections + :param username: Adafruit.IO Username for your account. + :param key: Adafruit IO access key (AIO Key) for your account. + :param secure: (optional, boolean) Switches secure/insecure connections + """ self._username = username self._service_host = service_host @@ -53,8 +59,8 @@ def __init__(self, username, key, service_host='io.adafruit.com', secure=True): self.on_disconnect = None self.on_message = None self.on_subscribe = None - # Initialize MQTT client. - self._client = mqtt.Client() + # Initialize v1 MQTT client. + self._client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1) if secure: self._client.tls_set_context() self._secure = True @@ -65,6 +71,7 @@ def __init__(self, username, key, service_host='io.adafruit.com', secure=True): self._client.on_connect = self._mqtt_connect self._client.on_disconnect = self._mqtt_disconnect self._client.on_message = self._mqtt_message + self._client.on_subscribe = self._mqtt_subscribe self._connected = False @@ -90,7 +97,7 @@ def _mqtt_disconnect(self, client, userdata, rc): # log the RC as an error. Continue on to call any disconnect handler # so clients can potentially recover gracefully. if rc != 0: - print("Unexpected disconnection.") + print('Unexpected disconnection.') raise MQTTError(rc) print('Disconnected from Adafruit IO!') # Call the on_disconnect callback if available. @@ -98,30 +105,41 @@ def _mqtt_disconnect(self, client, userdata, rc): self.on_disconnect(self) def _mqtt_message(self, client, userdata, msg): - logger.debug('Client on_message called.') """Parse out the topic and call on_message callback assume topic looks like `username/topic/id` + """ + logger.debug('Client on_message called.') parsed_topic = msg.topic.split('/') if self.on_message is not None: - topic = parsed_topic[2] - payload = '' if msg.payload is None else msg.payload.decode('utf-8') - elif self.on_message is not None and parsed_topic[0] == 'time': - topic = parsed_topic[0] - payload = msg.payload.decode('utf-8') - elif self.on_message is not None and parsed_topic[1] == 'groups': - topic = parsed_topic[3] - payload = msg.payload.decode('utf-8') + if parsed_topic[0] == 'time': + topic = parsed_topic[0] + payload = msg.payload.decode('utf-8') + elif parsed_topic[1] == 'groups': + topic = parsed_topic[3] + payload = msg.payload.decode('utf-8') + elif parsed_topic[2] == 'weather': + topic = parsed_topic[4] + payload = '' if msg.payload is None else msg.payload.decode('utf-8') + else: + topic = parsed_topic[2] + payload = '' if msg.payload is None else msg.payload.decode('utf-8') + else: + raise ValueError('on_message not defined') self.on_message(self, topic, payload) - - def _mqtt_subscribe(client, userdata, mid, granted_qos): + + def _mqtt_subscribe(self, client, userdata, mid, granted_qos): """Called when broker responds to a subscribe request.""" + logger.debug('Client called on_subscribe') + if self.on_subscribe is not None: + self.on_subscribe(self, userdata, mid, granted_qos) def connect(self, **kwargs): """Connect to the Adafruit.IO service. Must be called before any loop or publish operations are called. Will raise an exception if a connection cannot be made. Optional keyword arguments will be passed to paho-mqtt client connect function. + """ # Skip calling connect if already connected. if self._connected: @@ -134,6 +152,7 @@ def connect(self, **kwargs): def is_connected(self): """Returns True if connected to Adafruit.IO and False if not connected. + """ return self._connected @@ -146,9 +165,9 @@ def loop_background(self, stop=None): """Starts a background thread to listen for messages from Adafruit.IO and call the appropriate callbacks when feed events occur. Will return immediately and will not block execution. Should only be called once. - - Params: - - stop: boolean, stops the execution of the background loop. + + :param bool stop: Stops the execution of the background loop. + """ if stop: self._client.loop_stop() @@ -163,6 +182,7 @@ def loop_blocking(self): listen and respond to Adafruit.IO feed events. If you need to do other processing, consider using the loop_background function to run a loop in the background. + """ self._client.loop_forever() @@ -174,28 +194,59 @@ def loop(self, timeout_sec=1.0): The optional timeout_sec parameter specifies at most how long to block execution waiting for messages when this function is called. The default is one second. + """ self._client.loop(timeout=timeout_sec) - def subscribe(self, feed_id, feed_user=None): + def subscribe(self, feed_id, feed_user=None, qos=0): """Subscribe to changes on the specified feed. When the feed is updated the on_message function will be called with the feed_id and new value. - Params: - - feed_id: The id of the feed to subscribe to. - - feed_user (optional): The user id of the feed. Used for feed sharing functionality. + :param str feed_id: The key of the feed to subscribe to. + :param str feed_user: Optional, identifies feed owner. Used for feed sharing. + :param int qos: The QoS to use when subscribing. Defaults to 0. + """ + if qos > 1: + raise MQTTError("Adafruit IO only supports a QoS level of 0 or 1.") if feed_user is not None: - (res, mid) = self._client.subscribe('{0}/feeds/{1}'.format(feed_user, feed_id)) + (res, mid) = self._client.subscribe('{0}/feeds/{1}'.format(feed_user, feed_id, qos=qos)) else: - (res, mid) = self._client.subscribe('{0}/feeds/{1}'.format(self._username, feed_id)) + (res, mid) = self._client.subscribe('{0}/feeds/{1}'.format(self._username, feed_id), qos=qos) return res, mid - def subscribe_group(self, group_id): + def subscribe_group(self, group_id, qos=0): """Subscribe to changes on the specified group. When the group is updated the on_message function will be called with the group_id and the new value. + + :param str group_id: The id of the group to subscribe to. + :param int qos: The QoS to use when subscribing. Defaults to 0. + + """ + self._client.subscribe('{0}/groups/{1}'.format(self._username, group_id), qos=qos) + + def subscribe_randomizer(self, randomizer_id): + """Subscribe to changes on a specified random data stream from + Adafruit IO's random data service. + + MQTT random word subscriptions will publish data once per minute to + every client that is subscribed to the same topic. + + :param int randomizer_id: ID of the random word record you want data for. + """ - self._client.subscribe('{0}/groups/{1}'.format(self._username, group_id)) + self._client.subscribe('{0}/integration/words/{1}'.format(self._username, randomizer_id)) + + def subscribe_weather(self, weather_id, forecast_type): + """Subscribe to Adafruit IO Weather + :param int weather_id: weather record you want data for + :param string type: type of forecast data requested + """ + if forecast_type in forecast_types: + self._client.subscribe('{0}/integration/weather/{1}/{2}'.format(self._username, weather_id, forecast_type)) + else: + raise TypeError("Invalid Forecast Type Specified.") + return def subscribe_time(self, time): """Subscribe to changes on the Adafruit IO time feeds. When the feed is @@ -226,6 +277,15 @@ def unsubscribe(self, feed_id=None, group_id=None): raise TypeError('Invalid topic type specified.') return + def receive(self, feed_id): + """Receive the last published value from a specified feed. + + :param string feed_id: The ID of the feed to update. + :parm string value: The new value to publish to the feed + """ + (res, self._pub_mid) = self._client.publish('{0}/feeds/{1}/get'.format(self._username, feed_id), + payload='') + def publish(self, feed_id, value=None, group_id=None, feed_user=None): """Publish a value to a specified feed. diff --git a/LICENSE.md b/LICENSE.md index c841ffc..5ccd0cd 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (c) 2014-2018 Adafruit +Copyright (c) 2014-2019 Adafruit Author: Justin Cooper and Tony DiCola MIT License diff --git a/README.rst b/README.rst index 6d6e2ae..94f93ed 100644 --- a/README.rst +++ b/README.rst @@ -6,18 +6,21 @@ Adafruit IO Python :alt: Documentation Status .. image:: https://img.shields.io/discord/327254708534116352.svg - :target: https://discord.gg/nBQh6qu + :target: https://adafru.it/discord :alt: Chat -.. image:: https://travis-ci.org/adafruit/io-client-python.svg?branch=master - :target: https://travis-ci.org/adafruit/io-client-python +.. image:: https://github.com/adafruit/Adafruit_IO_Python/workflows/Build-CI/badge.svg + :target: https://github.com/adafruit/Adafruit_IO_Python/actions :alt: Build Status +.. image:: https://img.shields.io/badge/Try%20out-Adafruit%20IO%20Python-579ACA.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFkAAABZCAMAAABi1XidAAAB8lBMVEX///9XmsrmZYH1olJXmsr1olJXmsrmZYH1olJXmsr1olJXmsrmZYH1olL1olJXmsr1olJXmsrmZYH1olL1olJXmsrmZYH1olJXmsr1olL1olJXmsrmZYH1olL1olJXmsrmZYH1olL1olL0nFf1olJXmsrmZYH1olJXmsq8dZb1olJXmsrmZYH1olJXmspXmspXmsr1olL1olJXmsrmZYH1olJXmsr1olL1olJXmsrmZYH1olL1olLeaIVXmsrmZYH1olL1olL1olJXmsrmZYH1olLna31Xmsr1olJXmsr1olJXmsrmZYH1olLqoVr1olJXmsr1olJXmsrmZYH1olL1olKkfaPobXvviGabgadXmsqThKuofKHmZ4Dobnr1olJXmsr1olJXmspXmsr1olJXmsrfZ4TuhWn1olL1olJXmsqBi7X1olJXmspZmslbmMhbmsdemsVfl8ZgmsNim8Jpk8F0m7R4m7F5nLB6jbh7jbiDirOEibOGnKaMhq+PnaCVg6qWg6qegKaff6WhnpKofKGtnomxeZy3noG6dZi+n3vCcpPDcpPGn3bLb4/Mb47UbIrVa4rYoGjdaIbeaIXhoWHmZYHobXvpcHjqdHXreHLroVrsfG/uhGnuh2bwj2Hxk17yl1vzmljzm1j0nlX1olL3AJXWAAAAbXRSTlMAEBAQHx8gICAuLjAwMDw9PUBAQEpQUFBXV1hgYGBkcHBwcXl8gICAgoiIkJCQlJicnJ2goKCmqK+wsLC4usDAwMjP0NDQ1NbW3Nzg4ODi5+3v8PDw8/T09PX29vb39/f5+fr7+/z8/Pz9/v7+zczCxgAABC5JREFUeAHN1ul3k0UUBvCb1CTVpmpaitAGSLSpSuKCLWpbTKNJFGlcSMAFF63iUmRccNG6gLbuxkXU66JAUef/9LSpmXnyLr3T5AO/rzl5zj137p136BISy44fKJXuGN/d19PUfYeO67Znqtf2KH33Id1psXoFdW30sPZ1sMvs2D060AHqws4FHeJojLZqnw53cmfvg+XR8mC0OEjuxrXEkX5ydeVJLVIlV0e10PXk5k7dYeHu7Cj1j+49uKg7uLU61tGLw1lq27ugQYlclHC4bgv7VQ+TAyj5Zc/UjsPvs1sd5cWryWObtvWT2EPa4rtnWW3JkpjggEpbOsPr7F7EyNewtpBIslA7p43HCsnwooXTEc3UmPmCNn5lrqTJxy6nRmcavGZVt/3Da2pD5NHvsOHJCrdc1G2r3DITpU7yic7w/7Rxnjc0kt5GC4djiv2Sz3Fb2iEZg41/ddsFDoyuYrIkmFehz0HR2thPgQqMyQYb2OtB0WxsZ3BeG3+wpRb1vzl2UYBog8FfGhttFKjtAclnZYrRo9ryG9uG/FZQU4AEg8ZE9LjGMzTmqKXPLnlWVnIlQQTvxJf8ip7VgjZjyVPrjw1te5otM7RmP7xm+sK2Gv9I8Gi++BRbEkR9EBw8zRUcKxwp73xkaLiqQb+kGduJTNHG72zcW9LoJgqQxpP3/Tj//c3yB0tqzaml05/+orHLksVO+95kX7/7qgJvnjlrfr2Ggsyx0eoy9uPzN5SPd86aXggOsEKW2Prz7du3VID3/tzs/sSRs2w7ovVHKtjrX2pd7ZMlTxAYfBAL9jiDwfLkq55Tm7ifhMlTGPyCAs7RFRhn47JnlcB9RM5T97ASuZXIcVNuUDIndpDbdsfrqsOppeXl5Y+XVKdjFCTh+zGaVuj0d9zy05PPK3QzBamxdwtTCrzyg/2Rvf2EstUjordGwa/kx9mSJLr8mLLtCW8HHGJc2R5hS219IiF6PnTusOqcMl57gm0Z8kanKMAQg0qSyuZfn7zItsbGyO9QlnxY0eCuD1XL2ys/MsrQhltE7Ug0uFOzufJFE2PxBo/YAx8XPPdDwWN0MrDRYIZF0mSMKCNHgaIVFoBbNoLJ7tEQDKxGF0kcLQimojCZopv0OkNOyWCCg9XMVAi7ARJzQdM2QUh0gmBozjc3Skg6dSBRqDGYSUOu66Zg+I2fNZs/M3/f/Grl/XnyF1Gw3VKCez0PN5IUfFLqvgUN4C0qNqYs5YhPL+aVZYDE4IpUk57oSFnJm4FyCqqOE0jhY2SMyLFoo56zyo6becOS5UVDdj7Vih0zp+tcMhwRpBeLyqtIjlJKAIZSbI8SGSF3k0pA3mR5tHuwPFoa7N7reoq2bqCsAk1HqCu5uvI1n6JuRXI+S1Mco54YmYTwcn6Aeic+kssXi8XpXC4V3t7/ADuTNKaQJdScAAAAAElFTkSuQmCC + :target: https://mybinder.org/v2/gh/adafruit/adafruit_io_python_jupyter/master?filepath=adafruit-io-python-tutorial.ipynb + .. image:: https://cdn-learn.adafruit.com/assets/assets/000/057/153/original/adafruit_io_iopython.png?1530802073 -A Python client and examples for use with `io.adafruit.com `_. +A Python library and examples for use with `io.adafruit.com `_. -Compatible with Python Versions 3.4+ +Compatible with Python Versions 3.6+ Installation ================ @@ -32,6 +35,13 @@ If you have `PIP `_ installed (typica This will automatically install the Adafruit IO Python client code for your Python scripts to use. You might want to examine the examples folder in this GitHub repository to see examples of usage. +If the above command fails, you may first need to install prerequisites: + +.. code-block:: shell + + pip3 install setuptools + pip3 install wheel + Manual Installation ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/conf.py b/docs/conf.py index 0c56a44..42d0e3a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,8 +28,8 @@ master_doc = 'index' # General information about the project. -project = u'io-client-python' -copyright = u'2018 Adafruit Industries' +project = u'adafruit-io-python' +copyright = u'2023 Adafruit Industries' author = u'Adafruit Industries' # The version info for the project you're documenting, acts as replacement for @@ -37,16 +37,16 @@ # built documents. # # The short X.Y version. -version = u'2.0.1' +version = u'2.1.0' # The full version, including alpha/beta/rc tags. -release = u'2.0.1' +release = u'2.1.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -129,7 +129,7 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'io-client-pythonLibrary.tex', u'io-client-python Library Documentation', + (master_doc, 'adafruit-io-pythonLibrary.tex', u'adafruit-io-python Library Documentation', author, 'manual'), ] @@ -138,7 +138,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'io-client-pythonlibrary', u'io-client-python Library Documentation', + (master_doc, 'adafruit-io-pythonlibrary', u'adafruit-io-python Library Documentation', [author], 1) ] @@ -148,7 +148,7 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'io-client-pythonLibrary', u' io-client-python Library Documentation', - author, 'io-client-pythonLibrary', 'One line description of project.', + (master_doc, 'adafruit-io-pythonLibrary', u' adafruit-io-python Library Documentation', + author, 'adafruit-io-pythonLibrary', 'One line description of project.', 'Miscellaneous'), ] diff --git a/docs/data.rst b/docs/data.rst index ed76bf9..eb8fc65 100644 --- a/docs/data.rst +++ b/docs/data.rst @@ -33,6 +33,19 @@ You can get all of the data for a feed by using the ``data(feed)`` method. The r for d in data: print('Data value: {0}'.format(d.value)) +By default, the maximum number of data points returned is 1000. This limit can be changed by using the max_results parameter. + +.. code-block:: python + + # Get less than the default number of data points + data = aio.data('Test', max_results=100) + + # Get more than the default number of data points + data = aio.data('Test', max_results=2000) + + # Get all of the points + data = aio.data('Test', max_results=None) + 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. @@ -79,7 +92,9 @@ Data can be created after you create a feed, by using the ``send_batch_data(feed # Create a data items in the 'Test' feed. data_list = [Data(value=10), Data(value=11)] - aio.create_data('Test', data) + # send batch data + aio.send_batch_data(temperature.key, data_list) + Receive Data diff --git a/docs/feeds.rst b/docs/feeds.rst index 06adf5b..75cc525 100644 --- a/docs/feeds.rst +++ b/docs/feeds.rst @@ -11,7 +11,7 @@ Create a feed by constructing a Feed instance with at least a name specified, an # Import library and create instance of REST client. from Adafruit_IO import Client, Feed - aio = Client('YOUR ADAFRUIT IO KEY') + aio = Client('YOUR ADAFRUIT IO USERNAME', 'YOUR ADAFRUIT IO KEY') # Create Feed object with name 'Foo'. feed = Feed(name='Foo') @@ -30,7 +30,7 @@ You can get a list of your feeds by using the ``feeds()`` method which will retu # Import library and create instance of REST client. from Adafruit_IO import Client - aio = Client('YOUR ADAFRUIT IO KEY') + aio = Client('YOUR ADAFRUIT IO USERNAME', 'YOUR ADAFRUIT IO KEY') # Get list of feeds. feeds = aio.feeds() @@ -45,7 +45,7 @@ Alternatively you can retrieve the metadata for a single feed by calling ``feeds # Import library and create instance of REST client. from Adafruit_IO import Client - aio = Client('YOUR ADAFRUIT IO KEY') + aio = Client('YOUR ADAFRUIT IO USERNAME', 'YOUR ADAFRUIT IO KEY') # Get feed 'Foo' feed = aio.feeds('Foo') diff --git a/docs/groups.rst b/docs/groups.rst index b24901e..2a931de 100644 --- a/docs/groups.rst +++ b/docs/groups.rst @@ -19,7 +19,7 @@ Create a group by constructing a Group instance with at least a name specified, # Send the group for IO to create: # The returned object will contain all the details about the created group. - group = aio.create_group(group + group = aio.create_group(group) Group Retrieval diff --git a/examples/api/feeds.py b/examples/api/feeds.py index 4a9885b..848bdda 100644 --- a/examples/api/feeds.py +++ b/examples/api/feeds.py @@ -1,6 +1,6 @@ # Simple example of sending and receiving values from Adafruit IO with the REST # API client. -# Author: Tony Dicola, Justin Cooper +# Author: Tony Dicola, Justin Cooper, Brent Rubell # Import Adafruit IO REST client. from Adafruit_IO import Client, Feed @@ -19,18 +19,26 @@ aio = Client(ADAFRUIT_IO_USERNAME, ADAFRUIT_IO_KEY) # List all of your feeds +print("Obtaining user's feeds...") feeds = aio.feeds() -print(feeds) +print('Feeds: ', feeds) # Create a new feed +print("Creating new feed...") feed = Feed(name="PythonFeed") response = aio.create_feed(feed) -print(response) - -# List a specific feed -feed = aio.feeds(response.key) -print(feed) - +print("New feed: ", response) # Delete a feed aio.delete_feed(response.key) + +# Create feed in a group +feed = Feed(name="PythonGroupFeed") +group_key = "example" +print("Creating feed in group %s"%group_key) +response = aio.create_feed(feed, group_key) +print("New feed: ", response) + +# Delete a feed within a group +print("Deleting feed within group %s"%group_key) +aio.delete_feed(response.key) \ No newline at end of file diff --git a/examples/api/location.py b/examples/api/location.py index 7bec293..3e5846a 100644 --- a/examples/api/location.py +++ b/examples/api/location.py @@ -1,9 +1,8 @@ """ 'location.py' ================================== -Example of sending location over an -Adafruit IO feed to a Map Dashboard -block +Example of sending metadata +associated with a data point. Author(s): Brent Rubell """ @@ -12,9 +11,14 @@ from Adafruit_IO import Client, Feed, RequestError # Set to your Adafruit IO key. -ADAFRUIT_IO_USERNAME = 'YOUR_AIO_USERNAME' +# Remember, your key is a secret, +# so make sure not to publish it when you publish this code! ADAFRUIT_IO_KEY = 'YOUR_AIO_KEY' +# Set to your Adafruit IO username. +# (go to https://accounts.adafruit.com to find your username) +ADAFRUIT_IO_USERNAME = 'YOUR_AIO_USERNAME' + # Create an instance of the REST client. aio = Client(ADAFRUIT_IO_USERNAME, ADAFRUIT_IO_KEY) @@ -25,12 +29,12 @@ feed = Feed(name="location") location = aio.create_feed(feed) - -# Top Secret Adafruit HQ Location -value = 1 -lat = 40.726190 -lon = -74.005334 -ele = 6 # elevation above sea level (meters) +value = 42 +# Set metadata associated with value +metadata = {'lat': 40.726190, + 'lon': -74.005334, + 'ele': -6, + 'created_at': None} # Send location data to Adafruit IO -aio.send_location_data(location.key, value, lat, lon, ele) +aio.send_data(location.key, value, metadata) diff --git a/examples/api/random_data.py b/examples/api/random_data.py new file mode 100644 index 0000000..719e026 --- /dev/null +++ b/examples/api/random_data.py @@ -0,0 +1,28 @@ +""" +'random_data.py' +================================================ +Example for accessing the Adafruit IO Random +Data Service. + +Author(s): Brent Rubell for Adafruit Industries +""" +# Import JSON for forecast parsing +import json +# Import Adafruit IO REST client. +from Adafruit_IO import Client, Feed, RequestError + +# Set to your Adafruit IO key. +ADAFRUIT_IO_USERNAME = 'YOUR_IO_USERNAME' +ADAFRUIT_IO_KEY = 'YOUR_IO_KEY' + +# Create an instance of the REST client. +aio = Client(ADAFRUIT_IO_USERNAME, ADAFRUIT_IO_KEY) + +generator_id = 1461 + +# Get the specified randomizer record with its current value and related details. +random_data = aio.receive_random(generator_id) +# Parse the API response +data = json.dumps(random_data) +data = json.loads(data) +print('Random Data: {0}'.format(data['value'])) diff --git a/examples/api/weather.py b/examples/api/weather.py new file mode 100644 index 0000000..3e4bc5c --- /dev/null +++ b/examples/api/weather.py @@ -0,0 +1,41 @@ +""" +'weather.py' +================================================ +Dark Sky Hyperlocal for IO Plus +with Adafruit IO API + +Author(s): Brent Rubell for Adafruit Industries +""" +# Import JSON for forecast parsing +import json +# Import Adafruit IO REST client. +from Adafruit_IO import Client, Feed, RequestError + +# Set to your Adafruit IO key. +ADAFRUIT_IO_USERNAME = 'YOUR_IO_USERNAME' +ADAFRUIT_IO_KEY = 'YOUR_IO_PASSWORD' + +# Create an instance of the REST client. +aio = Client(ADAFRUIT_IO_USERNAME, ADAFRUIT_IO_KEY) + +# Grab the weather JSON +weather = aio.receive_weather(1234) +weather = json.dumps(weather) +forecast = json.loads(weather) + +# Parse the current forecast +current = forecast['current'] +print('Current Forecast') +print('It is {0} and {1}.'.format(current['summary'], current['temperature'])) + +# Parse the two day forecast +forecast_days_2 = forecast['forecast_days_2'] +print('\nWeather in Two Days') +print('It will be {0} with a high of {1}F and a low of {2}F.'.format( + forecast_days_2['summary'], forecast_days_2['temperatureLow'], forecast_days_2['temperatureHigh'])) + +# Parse the five day forecast +forecast_days_5 = forecast['forecast_days_5'] +print('\nWeather in Five Days') +print('It will be {0} with a high of {1}F and a low of {2}F.'.format( + forecast_days_5['summary'], forecast_days_5['temperatureLow'], forecast_days_5['temperatureHigh'])) \ No newline at end of file diff --git a/examples/basics/dashboard.py b/examples/basics/dashboard.py new file mode 100644 index 0000000..8a5bc61 --- /dev/null +++ b/examples/basics/dashboard.py @@ -0,0 +1,88 @@ +""" +'dashboard.py' +========================================= +Creates a dashboard with 3 blocks and feed it data + +Author(s): Doug Zobel +""" +from time import sleep +from random import randrange +from Adafruit_IO import Client, Feed, Block, Dashboard, Layout + +# Set to your Adafruit IO key. +# Remember, your key is a secret, +# so make sure not to publish it when you publish this code! +ADAFRUIT_IO_USERNAME = '' + +# Set to your Adafruit IO username. +# (go to https://accounts.adafruit.com to find your username) +ADAFRUIT_IO_KEY = '' + +# Create an instance of the REST client. +aio = Client(ADAFRUIT_IO_USERNAME, ADAFRUIT_IO_KEY) + +# Create a new feed named 'Dashboard Data' under the default group +feed = aio.create_feed(Feed(name="Dashboard Data"), "default") + +# Fetch group info (group.id needed when adding feeds to blocks) +group = aio.groups("default") + +# Create a new dasbhoard named 'Example Dashboard' +dashboard = aio.create_dashboard(Dashboard(name="Example Dashboard")) + +# Create a line_chart +linechart = Block(name="Linechart Data", + visual_type = 'line_chart', + properties = { + "gridLines": True, + "historyHours": "2"}, + block_feeds = [{ + "group_id": group.id, + "feed_id": feed.id + }]) +linechart = aio.create_block(dashboard.key, linechart) + +# Create a gauge +gauge = Block(name="Gauge Data", + visual_type = 'gauge', + block_feeds = [{ + "group_id": group.id, + "feed_id": feed.id + }]) +gauge = aio.create_block(dashboard.key, gauge) + +# Create a text stream +stream = Block(name="Stream Data", + visual_type = 'stream', + properties = { + "fontSize": "12", + "fontColor": "#63de00", + "showGroupName": "no"}, + block_feeds = [{ + "group_id": group.id, + "feed_id": feed.id + }]) +stream = aio.create_block(dashboard.key, stream) + +# Update the large layout to: +# |----------------| +# | Line Chart | +# |----------------| +# | Gauge | Stream | +# |----------------| +layout = Layout(lg = [ + {'x': 0, 'y': 0, 'w': 16, 'h': 4, 'i': str(linechart.id)}, + {'x': 0, 'y': 4, 'w': 8, 'h': 4, 'i': str(gauge.id)}, + {'x': 8, 'y': 4, 'w': 8, 'h': 4, 'i': str(stream.id)}]) +aio.update_layout(dashboard.key, layout) + +print("Dashboard created at: " + + "https://io.adafruit.com/{0}/dashboards/{1}".format(ADAFRUIT_IO_USERNAME, + dashboard.key)) +# Now send some data +value = 0 +while True: + value = (value + randrange(0, 10)) % 100 + print('sending data: ', value) + aio.send_data(feed.key, value) + sleep(3) diff --git a/examples/basics/digital_in.py b/examples/basics/digital_in.py index d2fdb4e..c4a40c0 100644 --- a/examples/basics/digital_in.py +++ b/examples/basics/digital_in.py @@ -51,4 +51,4 @@ aio.send(digital.key, button_current) # avoid timeout from adafruit io - time.sleep(1) + time.sleep(1) \ No newline at end of file diff --git a/examples/basics/digital_out.py b/examples/basics/digital_out.py index e92d3b6..5da4f99 100644 --- a/examples/basics/digital_out.py +++ b/examples/basics/digital_out.py @@ -40,7 +40,10 @@ while True: - data = aio.receive(digital.key) + try: + data = aio.receive(digital.key) + except RequestError as re: + pass # feed with no data will return 404 if int(data.value) == 1: print('received <- ON\n') elif int(data.value) == 0: diff --git a/examples/basics/environmental_monitor.py b/examples/basics/environmental_monitor.py deleted file mode 100644 index 0296ac8..0000000 --- a/examples/basics/environmental_monitor.py +++ /dev/null @@ -1,134 +0,0 @@ -""" -'environmental_monitor.py' -=============================================================================== -Example of sending I2C sensor data -from multiple sensors to Adafruit IO. - -Tutorial Link: https://learn.adafruit.com/adafruit-io-air-quality-monitor - -Adafruit invests time and resources providing this open source code. -Please support Adafruit and open source hardware by purchasing -products from Adafruit! - -Author(s): Brent Rubell for Adafruit Industries -Copyright (c) 2018 Adafruit Industries -Licensed under the MIT license. -All text above must be included in any redistribution. - -Dependencies: - - Adafruit_Blinka (CircuitPython, on Pi.) - (https://github.com/adafruit/Adafruit_Blinka) - - Adafruit_CircuitPython_SGP30. - (https://github.com/adafruit/Adafruit_CircuitPython_SGP30) - - Adafruit_CircuitPython_VEML6070. - (https://github.com/adafruit/Adafruit_CircuitPython_VEML6070) - - Adafruit_CircuitPython_BME280. - (https://github.com/adafruit/Adafruit_CircuitPython_BME280) -""" - -# Import standard python modules -import time - -# import Adafruit Blinka -import board -import busio - -# import sensor libraries -import adafruit_sgp30 -import adafruit_veml6070 -import adafruit_bme280 - -# import Adafruit IO REST client -from Adafruit_IO import Client, Feed, RequestError - -# loop timeout, in seconds. -LOOP_DELAY = 10 - -# Set to your Adafruit IO key. -# Remember, your key is a secret, -# so make sure not to publish it when you publish this code! -ADAFRUIT_IO_KEY = 'YOUR_AIO_KEY' - -# Set to your Adafruit IO username. -# (go to https://accounts.adafruit.com to find your username) -ADAFRUIT_IO_USERNAME = 'YOUR_AIO_USERNAME' - -# Create an instance of the REST client -aio = Client(ADAFRUIT_IO_USERNAME, ADAFRUIT_IO_KEY) - -try: # if we already have the feeds, assign them. - tvoc_feed = aio.feeds('tvoc') - eCO2_feed = aio.feeds('eco2') - uv_feed = aio.feeds('uv') - temperature_feed = aio.feeds('temperature') - humidity_feed = aio.feeds('humidity') - pressure_feed = aio.feeds('pressure') - altitude_feed = aio.feeds('altitude') -except RequestError: # if we don't, create and assign them. - tvoc_feed = aio.create_feed(Feed(name='tvoc')) - eCO2_feed = aio.create_feed(Feed(name='eco2')) - uv_feed = aio.create_feed(Feed(name='uv')) - temperature_feed = aio.create_feed(Feed(name='temperature')) - humidity_feed = aio.create_feed(Feed(name='humidity')) - pressure_feed = aio.create_feed(Feed(name='pressure')) - altitude_feed = aio.create_feed(Feed(name='altitude')) - -# Create busio I2C -i2c = busio.I2C(board.SCL, board.SDA, frequency=100000) -# Create VEML6070 object. -uv = adafruit_veml6070.VEML6070(i2c) -# Create BME280 object. -bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c) -bme280.sea_level_pressure = 1013.25 -# Create SGP30 object using I2C. -sgp30 = adafruit_sgp30.Adafruit_SGP30(i2c) -sgp30.iaq_init() -sgp30.set_iaq_baseline(0x8973, 0x8aae) - -# Sample VEML6070 -def sample_VEML(): - for j in range(10): - uv_raw = uv.read - return uv_raw - - -while True: - print('Reading sensors...') - # Read SGP30. - eCO2_data = sgp30.co2eq - tvoc_data = sgp30.tvoc - - # Read VEML6070. - uv_data = sample_VEML() - - # Read BME280. - temp_data = bme280.temperature - # convert temperature (C->F) - temp_data = int(temp_data) * 1.8 + 32 - humid_data = bme280.humidity - pressure_data = bme280.pressure - alt_data = bme280.altitude - - print('sending data to adafruit io...') - # Send SGP30 Data to Adafruit IO. - print('eCO2:', eCO2_data) - aio.send(eCO2_feed.key, eCO2_data) - print('tvoc:', tvoc_data) - aio.send(tvoc_feed.key, tvoc_data) - time.sleep(2) - # Send VEML6070 Data to Adafruit IO. - print('UV Level: ', uv_data) - aio.send(uv_feed.key, uv_data) - time.sleep(2) - # Send BME280 Data to Adafruit IO. - print('Temperature: %0.1f C' % temp_data) - aio.send(temperature_feed.key, temp_data) - print("Humidity: %0.1f %%" % humid_data) - aio.send(humidity_feed.key, int(humid_data)) - time.sleep(2) - print("Pressure: %0.1f hPa" % pressure_data) - aio.send(pressure_feed.key, int(pressure_data)) - print("Altitude = %0.2f meters" % alt_data) - aio.send(altitude_feed.key, int(alt_data)) - # avoid timeout from adafruit io - time.sleep(LOOP_DELAY * 60) diff --git a/examples/basics/location.py b/examples/basics/location.py index 24066f1..c772a9c 100644 --- a/examples/basics/location.py +++ b/examples/basics/location.py @@ -49,7 +49,8 @@ print('\tLon: ', lon) print('\tEle: ', ele) # Send location data to Adafruit IO - aio.send_location_data(location.key, value, lat, lon, ele) + metadata = { 'lat':lat, 'lon':lon, 'ele':ele, 'created_at':time.asctime(time.gmtime()) } + aio.send_data(location.key,value,metadata) # shift all values (for test/demo purposes) value += 1 lat -= 0.01 diff --git a/examples/basics/neopixel.py b/examples/basics/neopixel.py new file mode 100644 index 0000000..5c6b241 --- /dev/null +++ b/examples/basics/neopixel.py @@ -0,0 +1,69 @@ +""" +`rgb_led.py` +======================================================================= +Control a NeoPixel RGB LED using Adafruit IO and Python + +Tutorial Link: https://learn.adafruit.com/adafruit-io-basics-color + +Adafruit invests time and resources providing this open source code. +Please support Adafruit and open source hardware by purchasing +products from Adafruit! + +Author(s): Brent Rubell for Adafruit Industries +Copyright (c) 2023 Adafruit Industries +Licensed under the MIT license. +All text above must be included in any redistribution. + +Dependencies: + - Adafruit_Blinka + (https://github.com/adafruit/Adafruit_Blinka) + - Adafruit_CircuitPython_NeoPixel + (https://github.com/adafruit/Adafruit_CircuitPython_NeoPixel) +""" +import time +import board +import neopixel +from Adafruit_IO import Client, Feed, RequestError + +# Choose an open pin connected to the Data In of the NeoPixel strip, i.e. board.D18 +# NeoPixels must be connected to D10, D12, D18 or D21 to work. +pixel_pin = board.D18 + +# The number of NeoPixels +num_pixels = 1 + +# The order of the pixel colors - RGB or GRB. Some NeoPixels have red and green reversed! +ORDER = neopixel.GRB + +pixels = neopixel.NeoPixel( + pixel_pin, num_pixels, brightness=0.2, auto_write=False, pixel_order=ORDER +) + +# Set to your Adafruit IO key. +# Remember, your key is a secret, +# so make sure not to publish it when you publish this code! +ADAFRUIT_IO_KEY = 'YOUR_AIO_KEY' + +# Set to your Adafruit IO username. +# (go to https://accounts.adafruit.com to find your username) +ADAFRUIT_IO_USERNAME = 'YOUR_AIO_USERNAME' + +# Create an instance of the REST client. +aio = Client(ADAFRUIT_IO_USERNAME, ADAFRUIT_IO_KEY) + +try: # if we have a 'color' feed + color = aio.feeds('color') +except RequestError: # create an `color` feed + feed = Feed(name='color') + color = aio.create_feed(feed) + +while True: + # get the value of the Adafruit IO `color` feed + color_val = aio.receive(color.key) + # Print hex value + print('Received Color HEX: ', color_val) + pixels.fill(color_val.value) + pixels.show() + + # let's sleep/wait so we don't flood adafruit io's servers with requests + time.sleep(3) diff --git a/examples/basics/rgb_led.py b/examples/basics/rgb_led.py index e3bf6c6..06781bf 100644 --- a/examples/basics/rgb_led.py +++ b/examples/basics/rgb_led.py @@ -78,11 +78,11 @@ def map_range(x, in_min, in_max, out_min, out_max): if color_val != prev_color: # print rgb values and hex value print('Received Color: ') - red = aio.toRed(color_val.value) + red = aio.to_red(color_val.value) print('\t - R: ', red) - green = aio.toGreen(color_val.value) + green = aio.to_green(color_val.value) print('\t - G: ', green) - blue = aio.toBlue(color_val.value) + blue = aio.to_blue(color_val.value) print('\t - B: ', blue) print('\t - HEX: ', color_val.value) # map color values (0-255) to 16-bit values for the pca diff --git a/examples/basics/temp_humidity.py b/examples/basics/temp_humidity.py index a4fa881..618d5f6 100644 --- a/examples/basics/temp_humidity.py +++ b/examples/basics/temp_humidity.py @@ -1,8 +1,7 @@ """ 'temp_humidity.py' ================================== -Example of sending analog sensor -values to an Adafruit IO feed. +Example of sending temperature and humidity data to Adafruit IO Author(s): Brent Rubell @@ -11,24 +10,27 @@ Dependencies: - Adafruit IO Python Client (https://github.com/adafruit/io-client-python) - - Adafruit_Python_DHT - (https://github.com/adafruit/Adafruit_Python_DHT) + - Adafruit_CircuitPython_AHTx0 + (https://github.com/adafruit/Adafruit_CircuitPython_AHTx0) """ # import standard python modules. import time -# import adafruit dht library. -import Adafruit_DHT +# import adafruit-blinka modules +import board # import Adafruit IO REST client. -from Adafruit_IO import Client, Feed +from Adafruit_IO import Client, Feed, RequestError -# Delay in-between sensor readings, in seconds. -DHT_READ_TIMEOUT = 5 +# Import AHTx0 library +import adafruit_ahtx0 -# Pin connected to DHT22 data pin -DHT_DATA_PIN = 26 +# Set true to send tempertaure data in degrees fahrenheit ('f')? +USE_DEGREES_F = False + +# Time between sensor reads, in seconds +READ_TIMEOUT = 60 # Set to your Adafruit IO key. # Remember, your key is a secret, @@ -42,23 +44,40 @@ # Create an instance of the REST client. aio = Client(ADAFRUIT_IO_USERNAME, ADAFRUIT_IO_KEY) -# Set up Adafruit IO Feeds. -temperature_feed = aio.feeds('temperature') -humidity_feed = aio.feeds('humidity') +# Assign a temperature feed, if one exists already +try: + temperature_feed = aio.feeds('temperature') +except RequestError: # Doesn't exist, create a new feed + feed_temp = Feed(name="temperature") + temperature_feed = aio.create_feed(feed_temp) + +# Assign a humidity feed, if one exists already +try: + humidity_feed = aio.feeds('humidity') +except RequestError: # Doesn't exist, create a new feed + feed_humid = Feed(name="humidity") + humidity_feed = aio.create_feed(feed_humid) -# Set up DHT22 Sensor. -dht22_sensor = Adafruit_DHT.DHT22 +# Initialize the board's default I2C bus +i2c = board.I2C() # uses board.SCL and board.SDA +# Initialize AHT20 using the default address (0x38) and the board's default i2c bus +sensor = adafruit_ahtx0.AHTx0(i2c) while True: - humidity, temperature = Adafruit_DHT.read_retry(dht22_sensor, DHT_DATA_PIN) - if humidity is not None and temperature is not None: - print('Temp={0:0.1f}*C Humidity={1:0.1f}%'.format(temperature, humidity)) - # Send humidity and temperature feeds to Adafruit IO - temperature = '%.2f'%(temperature) - humidity = '%.2f'%(humidity) - aio.send(temperature_feed.key, str(temperature)) - aio.send(humidity_feed.key, str(humidity)) + temperature = sensor.temperature + humidity = sensor.relative_humidity + if USE_DEGREES_F: + temperature = temperature * 9.0 / 5.0 + 32.0 + print('Temp={0:0.1f}*F'.format(temperature)) else: - print('Failed to get DHT22 Reading, trying again in ', DHT_READ_TIMEOUT, 'seconds') + print('Temp={0:0.1f}*C'.format(temperature)) + print('Humidity={1:0.1f}%'.format(humidity)) + # Format sensor data as string for sending to Adafruit IO + temperature = '%.2f'%(temperature) + humidity = '%.2f'%(humidity) + # Send humidity and temperature data to Adafruit IO + aio.send(temperature_feed.key, str(temperature)) + aio.send(humidity_feed.key, str(humidity)) + # Timeout to avoid flooding Adafruit IO - time.sleep(DHT_READ_TIMEOUT) + time.sleep(READ_TIMEOUT) diff --git a/examples/basics/time-topics.py b/examples/basics/time.py similarity index 55% rename from examples/basics/time-topics.py rename to examples/basics/time.py index 3fd487a..a6204c8 100644 --- a/examples/basics/time-topics.py +++ b/examples/basics/time.py @@ -1,10 +1,11 @@ """ -`time-topics.py` -==================================== +`time.py` +========================================== Don't have a RTC handy and need accurate time measurements? -Let Adafruit IO serve real-time values! +Let Adafruit IO serve up real-time values +based off your device's IP-address! Author: Brent Rubell """ @@ -23,16 +24,8 @@ # Create an instance of the REST client. aio = Client(ADAFRUIT_IO_USERNAME, ADAFRUIT_IO_KEY) -print('---Adafruit IO REST API Time Helpers---') - -print('Seconds: aio.receive_time(seconds)') -secs_val = aio.receive_time('seconds') -print('\t' + secs_val) - -print('Milliseconds: aio.receive_time(millis)') -ms_val = aio.receive_time('millis') -print('\t' + ms_val) - -print('ISO-8601: aio.receive_time(ISO-8601)') -iso_val = aio.receive_time('ISO-8601') -print('\t' + iso_val) \ No newline at end of file +# Get the time from Adafruit IO +time = aio.receive_time() +# Time is returned as a `struct_time` +# https://docs.python.org/3.7/library/time.html#time.struct_time +print(time) \ No newline at end of file diff --git a/examples/mqtt/mqtt_shared_feeds.py b/examples/mqtt/mqtt_shared_feeds.py index 38b6667..23b2f16 100644 --- a/examples/mqtt/mqtt_shared_feeds.py +++ b/examples/mqtt/mqtt_shared_feeds.py @@ -70,5 +70,5 @@ def message(client, feed_id, payload): while True: value = random.randint(0, 100) print('Publishing {0} to {1}.'.format(value, IO_FEED)) - client.publish(IO_FEED, value, IO_FEED_USERNAME) + client.publish(IO_FEED, value, feed_user=IO_FEED_USERNAME) time.sleep(10) diff --git a/examples/mqtt/mqtt_subscribe.py b/examples/mqtt/mqtt_subscribe.py index fcda0ab..e3b2999 100644 --- a/examples/mqtt/mqtt_subscribe.py +++ b/examples/mqtt/mqtt_subscribe.py @@ -31,6 +31,10 @@ def connected(client): # Subscribe to changes on a feed named DemoFeed. client.subscribe(FEED_ID) +def subscribe(client, userdata, mid, granted_qos): + # This method is called when the client subscribes to a new feed. + print('Subscribed to {0} with QoS {1}'.format(FEED_ID, granted_qos[0])) + def disconnected(client): # Disconnected function will be called when the client disconnects. print('Disconnected from Adafruit IO!') @@ -50,6 +54,7 @@ def message(client, feed_id, payload): client.on_connect = connected client.on_disconnect = disconnected client.on_message = message +client.on_subscribe = subscribe # Connect to the Adafruit IO server. client.connect() diff --git a/examples/mqtt/mqtt_time.py b/examples/mqtt/mqtt_time.py index e91f820..19875d3 100644 --- a/examples/mqtt/mqtt_time.py +++ b/examples/mqtt/mqtt_time.py @@ -48,17 +48,14 @@ def message(client, feed_id, payload): # Connect to the Adafruit IO server. client.connect() -# time per loop -loop_time = 2 +# Subscribe to the time feeds +print('* Subscribing to time/seconds') +client.subscribe_time('seconds') -client.loop_background() -while True: - print('* Subscribing to /time/seconds') - client.subscribe_time('seconds') - time.sleep(loop_time) - print('* Subscribing to /time/millis') - client.subscribe_time('millis') - time.sleep(loop_time) - print('* Subscribing to iso-8601') - client.subscribe_time('iso') - time.sleep(loop_time) +print('* Subscribing to time/millis') +client.subscribe_time('millis') + +print('* Subscribing to time/ISO-8601') +client.subscribe_time('iso') + +client.loop_blocking() diff --git a/examples/mqtt/mqtt_viewall.py b/examples/mqtt/mqtt_viewall.py index 64392be..31c3a9e 100644 --- a/examples/mqtt/mqtt_viewall.py +++ b/examples/mqtt/mqtt_viewall.py @@ -50,12 +50,12 @@ def on_connect(client, userdata, flags, rc): def on_disconnect(client, userdata, rc): print('Disconnected!') -def on_message(client, userdata, msg, retain): +def on_message(client, userdata, msg): print('Received on {0}: {1}'.format(msg.topic, msg.payload.decode('utf-8'))) -# Create MQTT client and connect to Adafruit IO. -client = mqtt.Client() +# Create Paho v1 MQTT client and connect to Adafruit IO. +client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1) client.username_pw_set(USERNAME, KEY) client.on_connect = on_connect client.on_disconnect = on_disconnect diff --git a/examples/mqtt/mqtt_weather.py b/examples/mqtt/mqtt_weather.py new file mode 100644 index 0000000..9d148d4 --- /dev/null +++ b/examples/mqtt/mqtt_weather.py @@ -0,0 +1,125 @@ +""" +Example of using the Adafruit IO MQTT Client +for subscribing to the Adafruit IO Weather Service +Note: This feature is avaliable for IO Plus Subscribers ONLY + +API Documentation: https://io.adafruit.com/services/weather + +Author: Brent Rubell for Adafruit Industries +""" + +# Import standard python modules. +import sys +import json + +# Import Adafruit IO MQTT client. +from Adafruit_IO import MQTTClient + +# Set to your Adafruit IO key. +# Remember, your key is a secret, +# so make sure not to publish it when you publish this code! +ADAFRUIT_IO_KEY = 'KEY' + +# Set to your Adafruit IO username. +# (go to https://accounts.adafruit.com to find your username) +ADAFRUIT_IO_USERNAME = 'USER' + +# Set to ID of the forecast to subscribe to for updates +forecast_id = 2153 + +# Set to the ID of the feed to subscribe to for updates. +""" +Valid forecast types are: +current +forecast_minutes_5 +forecast_minutes_30 +forecast_hours_1 +forecast_hours_2 +forecast_hours_6 +forecast_hours_24 +forecast_days_1 +forecast_days_2 +forecast_days_5 +""" +# Subscribe to the current forecast +forecast_today = 'current' +# Subscribe to tomorrow's forecast +forecast_two_days = 'forecast_days_2' +# Subscribe to forecast in 5 days +forecast_in_5_days = 'forecast_days_5' + +# Define callback functions which will be called when certain events happen. +# pylint: disable=redefined-outer-name +def connected(client): + # Connected function will be called when the client is connected to Adafruit IO. + # This is a good place to subscribe to feed changes. The client parameter + # passed to this function is the Adafruit IO MQTT client so you can make + # calls against it easily. + print('Connected to Adafruit IO! Listening to forecast: {0}...'.format(forecast_id)) + # Subscribe to changes on the current forecast. + client.subscribe_weather(forecast_id, forecast_today) + + # Subscribe to changes on tomorrow's forecast. + client.subscribe_weather(forecast_id, forecast_two_days) + + # Subscribe to changes on forecast in 5 days. + client.subscribe_weather(forecast_id, forecast_in_5_days) + +# pylint: disable=unused-argument +def disconnected(client): + # Disconnected function will be called when the client disconnects. + print('Disconnected from Adafruit IO!') + sys.exit(1) + +# pylint: disable=unused-argument +def message(client, topic, payload): + """Message function will be called when any subscribed forecast has an update. + Weather data is updated at most once every 20 minutes. + """ + # forecast based on mqtt topic + if topic == 'current': + # Print out today's forecast + today_forecast = payload + print('\nCurrent Forecast') + parseForecast(today_forecast) + elif topic == 'forecast_days_2': + # Print out tomorrow's forecast + two_day_forecast = payload + print('\nWeather in Two Days') + parseForecast(two_day_forecast) + elif topic == 'forecast_days_5': + # Print out forecast in 5 days + five_day_forecast = payload + print('\nWeather in 5 Days') + parseForecast(five_day_forecast) + +def parseForecast(forecast_data): + """Parses and prints incoming forecast data + """ + # incoming data is a utf-8 string, encode it as a json object + forecast = json.loads(forecast_data) + # Print out the forecast + try: + print('It is {0} and {1}F.'.format(forecast['summary'], forecast['temperature'])) + except KeyError: + # future weather forecasts return a high and low temperature, instead of 'temperature' + print('It will be {0} with a high of {1}F and a low of {2}F.'.format( + forecast['summary'], forecast['temperatureLow'], forecast['temperatureHigh'])) + print('with humidity of {0}%, wind speed of {1}mph, and {2}% chance of precipitation.'.format( + forecast['humidity'], forecast['windSpeed'], forecast['precipProbability'])) + +# Create an MQTT client instance. +client = MQTTClient(ADAFRUIT_IO_USERNAME, ADAFRUIT_IO_KEY) + +# Setup the callback functions defined above. +client.on_connect = connected +client.on_disconnect = disconnected +client.on_message = message + +# Connect to the Adafruit IO server. +client.connect() + +# Start a message loop that blocks forever waiting for MQTT messages to be +# received. Note there are other options for running the event loop like doing +# so in a background thread--see the mqtt_client.py example to learn more. +client.loop_blocking() diff --git a/setup.py b/setup.py index f848d1e..09087f3 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ long_description = open('README.rst').read(), long_description_content_type='text/x-rst', - url = 'https://github.com/adafruit/io-client-python', + url = 'https://github.com/adafruit/Adafruit_IO_Python', author = 'Adafruit Industries', author_email = 'adafruitio@adafruit.com', @@ -64,6 +64,6 @@ packages = ['Adafruit_IO'], py_modules = ['ez_setup'], - keywords = 'adafruitio io python circuitpython raspberrypi hardware MQTT', + keywords = 'adafruitio io python circuitpython raspberrypi hardware MQTT REST', classifiers = classifiers ) diff --git a/tests/base.py b/tests/base.py index 31d2ceb..ac51fab 100644 --- a/tests/base.py +++ b/tests/base.py @@ -28,7 +28,7 @@ def get_test_key(self): """Return the AIO key specified in the ADAFRUIT_IO_KEY environment variable, or raise an exception if it doesn't exist. """ - key = '68163f6f6ee24475b5edb0ed1f77f80a' + key = os.environ.get('ADAFRUIT_IO_KEY', None) if key is None: raise RuntimeError("ADAFRUIT_IO_KEY environment variable must be " \ "set with valid Adafruit IO key to run this test!") @@ -38,7 +38,7 @@ def get_test_username(self): """Return the AIO username specified in the ADAFRUIT_IO_USERNAME environment variable, or raise an exception if it doesn't exist. """ - username = 'travisiotester' + username = os.environ.get('ADAFRUIT_IO_USERNAME', None) if username is None: raise RuntimeError("ADAFRUIT_IO_USERNAME environment variable must be " \ "set with valid Adafruit IO username to run this test!") diff --git a/tests/test_client.py b/tests/test_client.py index fd75b27..7714909 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -3,7 +3,7 @@ import time import unittest -from Adafruit_IO import Client, Data, Feed, Group, RequestError +from Adafruit_IO import Client, Data, Feed, Group, Dashboard, Block, Layout, RequestError import base @@ -28,7 +28,7 @@ class TestClient(base.IOTestCase): # Helper Methods def get_client(self): # Construct an Adafruit IO REST client and return it. - return Client(self.get_test_username(), self.get_test_key(), proxies=PROXIES, base_url=BASE_URL, api_version = "v2") + return Client(self.get_test_username(), self.get_test_key(), proxies=PROXIES, base_url=BASE_URL) def ensure_feed_deleted(self, client, feed): # Delete the specified feed if it exists. @@ -46,9 +46,25 @@ def ensure_group_deleted(self, client, group): # Swallow the error if the group doesn't exist. pass + def ensure_dashboard_deleted(self, client, dashboard): + # Delete the specified dashboard if it exists. + try: + client.delete_dashboard(dashboard) + except RequestError: + # Swallow the error if the dashboard doesn't exist. + pass + + def ensure_block_deleted(self, client, dashboard, block): + # Delete the specified block if it exists. + try: + client.delete_block(dashboard, block) + except RequestError: + # Swallow the error if the block 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) + data = client.data(feed, max_results=None) for d in data: client.delete(feed, d.id) @@ -80,6 +96,8 @@ def test_send_batch_data(self): self.assertEqual(int(data.value), 42) def test_receive_next(self): + """receive_next + """ io = self.get_client() self.ensure_feed_deleted(io, 'testfeed') test_feed = io.create_feed(Feed(name="testfeed")) @@ -88,6 +106,8 @@ def test_receive_next(self): self.assertEqual(int(data.value), 1) def test_receive_previous(self): + """receive_previous + """ io = self.get_client() self.ensure_feed_deleted(io, 'testfeed') test_feed = io.create_feed(Feed(name="testfeed")) @@ -101,6 +121,8 @@ def test_receive_previous(self): self.assertEqual(int(data.value), 2) def test_data_on_feed_returns_all_data(self): + """send_data + """ io = self.get_client() self.ensure_feed_deleted(io, 'testfeed') test_feed = io.create_feed(Feed(name="testfeed")) @@ -112,6 +134,8 @@ def test_data_on_feed_returns_all_data(self): self.assertEqual(int(result[1].value), 1) def test_data_on_feed_and_data_id_returns_data(self): + """send_data + """ io = self.get_client() self.ensure_feed_deleted(io, 'testfeed') test_feed = io.create_feed(Feed(name="testfeed")) @@ -121,6 +145,8 @@ def test_data_on_feed_and_data_id_returns_data(self): self.assertEqual(int(data.value), int(result.value)) def test_create_data(self): + """create_data + """ aio = self.get_client() self.ensure_feed_deleted(aio, 'testfeed') test_feed = aio.create_feed(Feed(name="testfeed")) @@ -128,18 +154,63 @@ def test_create_data(self): data = Data(value=42) result = aio.create_data('testfeed', data) self.assertEqual(int(result.value), 42) - + def test_location_data(self): + """receive_location + """ aio = self.get_client() self.ensure_feed_deleted(aio, 'testlocfeed') - test_feed = aio.create_feed(Feed(name="testlocfeed")) - aio.send_location_data(test_feed.key, 0, 40, -74, 6) + test_feed = aio.create_feed(Feed(name='testlocfeed')) + metadata = {'lat': 40.726190, + 'lon': -74.005334, + 'ele': -6, + 'created_at': None} + aio.send_data(test_feed.key, 40, metadata) data = aio.receive(test_feed.key) - self.assertEqual(int(data.value), 0) - self.assertEqual(float(data.lat), 40.0) - self.assertEqual(float(data.lon), -74.0) - self.assertEqual(float(data.ele), 6.0) - + self.assertEqual(int(data.value), 40) + self.assertEqual(float(data.lat), 40.726190) + self.assertEqual(float(data.lon), -74.005334) + self.assertEqual(float(data.ele), -6.0) + + def test_time_data(self): + """receive_time + """ + aio = self.get_client() + server_time = aio.receive_time(timezone='UTC') + # Check that each value is rx'd properly + # (should never be None type) + for time_data in server_time: + self.assertIsNotNone(time_data) + # Check that the week day was interpreted properly + adjusted_time = time.localtime(time.mktime(server_time)) + self.assertEqual(server_time.tm_wday, adjusted_time.tm_wday) + + def test_parse_time_struct(self): + """Ensure the _parse_time_struct method properly handles all 7 + week days. Particularly important to make sure Sunday is 6, + not -1""" + # Zero time is a dictionary as would be provided by server + # (wday is one higher than it should be) + zero_time = {'year': 1970, + 'mon': 1, + 'mday': 1, + 'hour': 0, + 'min': 0, + 'sec': 0, + 'wday': 4, + 'yday': 1, + 'isdst': 0} + + # Create a good struct for each day of the week and make sure + # the server-style dictionary is parsed correctly + for k in range(7): + real_struct = time.gmtime(k * 86400) + d = zero_time.copy() + d['mday'] += k + d['wday'] += k + d['yday'] += k + newd = Client._parse_time_struct(d) + self.assertEqual(newd.tm_wday, real_struct.tm_wday) # Test Feed Functionality def test_append_by_feed_name(self): @@ -163,6 +234,22 @@ def test_create_feed(self): result = io.create_feed(feed) self.assertEqual(result.name, 'testfeed') + def test_create_feed_in_group(self): + """Tests creating a feed within a group. + + """ + io = self.get_client() + self.ensure_feed_deleted(io, 'testfeed') + self.ensure_group_deleted(io, 'testgroup') + + group = io.create_group(Group(name='testgroup')) + feed = Feed(name='testfeed') + result = io.create_feed(feed, "testgroup") + self.assertEqual(result.key, "testgroup.testfeed") + + io.delete_feed(result.key) + io.delete_group('testgroup') + def test_feeds_returns_all_feeds(self): io = self.get_client() self.ensure_feed_deleted(io, 'testfeed') @@ -228,3 +315,95 @@ def test_receive_group_by_key(self): group = io.create_group(Group(name='grouprx')) response = io.groups(group.key) self.assertEqual(response.key, 'grouprx') + + + # Test Dashboard Functionality + def test_dashboard_create_dashboard(self): + io = self.get_client() + self.ensure_dashboard_deleted(io, 'dashtest') + response = io.create_dashboard(Dashboard(name='dashtest')) + self.assertEqual(response.name, 'dashtest') + + def test_dashboard_returns_all_dashboards(self): + io = self.get_client() + self.ensure_dashboard_deleted(io, 'dashtest') + dashboard = io.create_dashboard(Dashboard(name='dashtest')) + response = io.dashboards() + self.assertGreaterEqual(len(response), 1) + + def test_dashboard_returns_requested_feed(self): + io = self.get_client() + self.ensure_dashboard_deleted(io, 'dashtest') + dashboard = io.create_dashboard(Dashboard(name='dashtest')) + response = io.dashboards('dashtest') + self.assertEqual(response.name, 'dashtest') + + # Test Block Functionality + def test_block_create_block(self): + io = self.get_client() + self.ensure_block_deleted(io, 'dashtest', 'blocktest') + self.ensure_dashboard_deleted(io, 'dashtest') + dash = io.create_dashboard(Dashboard(name='dashtest')) + block = io.create_block(dash.key, Block(name='blocktest', + visual_type = 'line_chart')) + self.assertEqual(block.name, 'blocktest') + io.delete_block(dash.key, block.id) + io.delete_dashboard(dash.key) + + def test_dashboard_returns_all_blocks(self): + io = self.get_client() + self.ensure_block_deleted(io, 'dashtest', 'blocktest') + self.ensure_dashboard_deleted(io, 'dashtest') + dash = io.create_dashboard(Dashboard(name='dashtest')) + block = io.create_block(dash.key, Block(name='blocktest', + visual_type = 'line_chart')) + response = io.blocks(dash.key) + self.assertEqual(len(response), 1) + io.delete_block(dash.key, block.id) + io.delete_dashboard(dash.key) + + def test_dashboard_returns_requested_block(self): + io = self.get_client() + self.ensure_block_deleted(io, 'dashtest', 'blocktest') + self.ensure_dashboard_deleted(io, 'dashtest') + dash = io.create_dashboard(Dashboard(name='dashtest')) + block = io.create_block(dash.key, Block(name='blocktest', + visual_type = 'line_chart')) + response = io.blocks(dash.key, block.id) + self.assertEqual(response.name, 'blocktest') + io.delete_block(dash.key, block.id) + io.delete_dashboard(dash.key) + + # Test Layout Functionality + def test_layout_returns_all_layouts(self): + io = self.get_client() + self.ensure_block_deleted(io, 'dashtest', 'blocktest') + self.ensure_dashboard_deleted(io, 'dashtest') + dash = io.create_dashboard(Dashboard(name='dashtest')) + block = io.create_block(dash.key, Block(name='blocktest', + visual_type = 'line_chart')) + response = io.layouts(dash.key) + self.assertEqual(len(response), 5) # 5 layouts: xs, sm, md, lg, xl + self.assertEqual(len(response.lg), 1) + io.delete_block(dash.key, block.id) + io.delete_dashboard(dash.key) + + def test_layout_update_layout(self): + io = self.get_client() + self.ensure_block_deleted(io, 'dashtest', 'blocktest') + self.ensure_dashboard_deleted(io, 'dashtest') + dash = io.create_dashboard(Dashboard(name='dashtest')) + block = io.create_block(dash.key, Block(name='blocktest', + visual_type = 'line_chart')) + layout = Layout(lg = [ + {'x': 0, 'y': 0, 'w': 16, 'h': 4, 'i': str(block.id)}]) + io.update_layout(dash.key, layout) + response = io.layouts(dash.key) + self.assertEqual(len(response.lg), 1) + self.assertEqual(response.lg[0]['w'], 16) + io.delete_block(dash.key, block.id) + io.delete_dashboard(dash.key) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_model.py b/tests/test_model.py index 623bd99..02b105e 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -18,7 +18,7 @@ # 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, Feed, Group +from Adafruit_IO import Data, Feed, Group, Dashboard, Block, Layout import base @@ -45,11 +45,12 @@ def test_data_properties_are_optional(self): def test_feeds_have_explicitly_set_values(self): """ Let's make sure feeds are explicitly set from within the model: - Feed.__new__.__defaults__ = (None, None, None, None, None, 'ON', 'Private', None, None, None) + Feed.__new__.__defaults__ = (None, None, None, None, None, None, 'ON', 'Private', None, None, None) """ feed = Feed(name='foo') self.assertEqual(feed.name, 'foo') self.assertIsNone(feed.key) + self.assertIsNone(feed.id) self.assertIsNone(feed.description) self.assertIsNone(feed.unit_type) self.assertIsNone(feed.unit_symbol) @@ -69,6 +70,40 @@ def test_group_properties_are_optional(self): self.assertIsNone(group.feeds) self.assertIsNone(group.properties) + """ Let's make sure feeds are explicitly set from within the model: + Dashboard.__new__.__defaults__ = (None, None, None, False, "dark", True, None, None) + + """ + def test_dashboard_have_explicitly_set_values(self): + dashboard = Dashboard(name="foo") + self.assertEqual(dashboard.name, 'foo') + self.assertIsNone(dashboard.key) + self.assertIsNone(dashboard.description) + self.assertFalse(dashboard.show_header) + self.assertEqual(dashboard.color_mode, 'dark') + self.assertTrue(dashboard.block_borders) + self.assertIsNone(dashboard.header_image_url) + self.assertIsNone(dashboard.blocks) + + """ Let's make sure feeds are explicitly set from within the model: + Block.__new__.__defaults__ = (None, None, None {}, None) + """ + def test_block_have_explicitly_set_values(self): + block = Block(name="foo") + self.assertEqual(block.name, 'foo') + self.assertIsNone(block.id) + self.assertIsNone(block.visual_type) + self.assertEqual(type(block.properties), dict) + self.assertEqual(len(block.properties), 0) + self.assertIsNone(block.block_feeds) + + def test_layout_properties_are_optional(self): + layout = Layout() + self.assertIsNone(layout.xl) + self.assertIsNone(layout.lg) + self.assertIsNone(layout.md) + self.assertIsNone(layout.sm) + self.assertIsNone(layout.xs) def test_from_dict_ignores_unknown_items(self): data = Data.from_dict({'value': 'foo', 'feed_id': 10, 'unknown_param': 42})