From 1498e285e6da21d78f07f2c529fd3c391c846979 Mon Sep 17 00:00:00 2001 From: Tyeth Gundry Date: Tue, 16 Jul 2024 14:53:17 +0100 Subject: [PATCH 01/12] IO_HTTP: Support creating group data with `send_group_data` Fixes #97 --- adafruit_io/adafruit_io.py | 21 ++++++++++++- .../adafruit_io_http/adafruit_io_groups.py | 31 ++++++++++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/adafruit_io/adafruit_io.py b/adafruit_io/adafruit_io.py index 67b014c..176255c 100755 --- a/adafruit_io/adafruit_io.py +++ b/adafruit_io/adafruit_io.py @@ -631,10 +631,29 @@ def send_batch_data(self, feed_key: str, data_list: list): :param list Data: Data list to send """ validate_feed_key(feed_key) - path = "feeds/{0}/data/batch".format(feed_key) + path = self._compose_path("feeds/{0}/data/batch".format(feed_key)) data_dict = type(data_list)((data._asdict() for data in data_list)) self._post(path, {"data": data_dict}) + def send_group_data( + self, group_key: str, feeds_and_data: list, metadata: Optional[dict] = None + ): + """ + Sends data to specified Adafruit IO feeds in a group + + :param str group_key: Adafruit IO feed key + :param list feeds_and_data: A list of dicts, with feed "key" and "value" entries + :param dict metadata: Optional metadata associated with the data e.g. created_at, lat, lon, ele + """ + validate_feed_key(group_key) + path = self._compose_path("groups/{0}/data".format(group_key)) + if not isinstance(feeds_and_data, list): + raise ValueError("This method accepts a list of dicts with \"key\" and \"value\".") + if metadata is not None: + self._post(path, {**metadata, "feeds": feeds_and_data}) + else: + self._post(path, {"feeds": feeds_and_data}) + def receive_all_data(self, feed_key: str): """ Get all data values from a specified Adafruit IO feed. Data is diff --git a/examples/adafruit_io_http/adafruit_io_groups.py b/examples/adafruit_io_http/adafruit_io_groups.py index 7398605..06ac1f7 100644 --- a/examples/adafruit_io_http/adafruit_io_groups.py +++ b/examples/adafruit_io_http/adafruit_io_groups.py @@ -4,6 +4,7 @@ # Adafruit IO HTTP API - Group Interactions # Documentation: https://io.adafruit.com/api/docs/#groups # adafruit_circuitpython_adafruitio with an esp32spi_socket +import datetime import board import busio from digitalio import DigitalInOut @@ -50,6 +51,13 @@ ssl_context = adafruit_connection_manager.get_radio_ssl_context(esp) requests = adafruit_requests.Session(pool, ssl_context) +# If you are testing on python with blinka, use real requests below and comment out above: +# import os, datetime, requests as real_requests +# from adafruit_io.adafruit_io import IO_HTTP +# secrets = {"aio_username": os.getenv("AIO_USERNAME"), "aio_key": os.getenv("AIO_KEY")} +# requests = real_requests.Session() + + # Set your Adafruit IO Username and Key in secrets.py # (visit io.adafruit.com if you need to create an account, # or if you need your Adafruit IO key.) @@ -72,8 +80,29 @@ humidity_feed = io.create_new_feed("humidity", "a feed for humidity data") io.add_feed_to_group(sensor_group["key"], humidity_feed["key"]) +# show humidity feed is in two groups +print("Getting fresh humidity feed info... (notice groups)") +print(io.get_feed(humidity_feed["key"])) + +# Publish data for multiple feeds to a group, use different timestamps for no reason +print("Publishing batch data to group feeds with created_at set 99minutes ago...") +thetime = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(minutes=99) +io.send_group_data( + group_key=sensor_group["key"], + feeds_and_data=[ + {"key": "temperature", "value": 20.0}, + {"key": "humidity", "value": 40.0}, + ], + metadata={ + "lat": 50.1858942, + "lon": -4.9677478, + "ele": 4, + "created_at": thetime.isoformat(), + }, +) + # Get info from the group -print("Getting fresh group info...") +print("Getting fresh group info... (notice created_at vs updated_at)") sensor_group = io.get_group("envsensors") # refresh data via HTTP API print(sensor_group) From 7191b1bd3196b7d6fb376f21647b3b1903e4b757 Mon Sep 17 00:00:00 2001 From: Tyeth Gundry Date: Tue, 16 Jul 2024 14:57:44 +0100 Subject: [PATCH 02/12] formatting --- adafruit_io/adafruit_io.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/adafruit_io/adafruit_io.py b/adafruit_io/adafruit_io.py index 176255c..00745e2 100755 --- a/adafruit_io/adafruit_io.py +++ b/adafruit_io/adafruit_io.py @@ -648,7 +648,9 @@ def send_group_data( validate_feed_key(group_key) path = self._compose_path("groups/{0}/data".format(group_key)) if not isinstance(feeds_and_data, list): - raise ValueError("This method accepts a list of dicts with \"key\" and \"value\".") + raise ValueError( + 'This method accepts a list of dicts with "key" and "value".' + ) if metadata is not None: self._post(path, {**metadata, "feeds": feeds_and_data}) else: From 3af0502096b49004ac4c4c9def4cc33db088c00a Mon Sep 17 00:00:00 2001 From: Tyeth Gundry Date: Tue, 16 Jul 2024 14:59:21 +0100 Subject: [PATCH 03/12] adjust docstring --- adafruit_io/adafruit_io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_io/adafruit_io.py b/adafruit_io/adafruit_io.py index 00745e2..e59b4c7 100755 --- a/adafruit_io/adafruit_io.py +++ b/adafruit_io/adafruit_io.py @@ -643,7 +643,7 @@ def send_group_data( :param str group_key: Adafruit IO feed key :param list feeds_and_data: A list of dicts, with feed "key" and "value" entries - :param dict metadata: Optional metadata associated with the data e.g. created_at, lat, lon, ele + :param dict metadata: Optional metadata for the data e.g. created_at, lat, lon, ele """ validate_feed_key(group_key) path = self._compose_path("groups/{0}/data".format(group_key)) From ac84b37052461f1c29d1aeed841b3d5688cc5714 Mon Sep 17 00:00:00 2001 From: Tyeth Gundry Date: Tue, 16 Jul 2024 16:01:10 +0100 Subject: [PATCH 04/12] fix metadata logic + saftey check --- adafruit_io/adafruit_io.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/adafruit_io/adafruit_io.py b/adafruit_io/adafruit_io.py index e59b4c7..085273c 100755 --- a/adafruit_io/adafruit_io.py +++ b/adafruit_io/adafruit_io.py @@ -652,7 +652,10 @@ def send_group_data( 'This method accepts a list of dicts with "key" and "value".' ) if metadata is not None: - self._post(path, {**metadata, "feeds": feeds_and_data}) + if not isinstance(metadata, dict): + raise ValueError("Metadata must be a dictionary.") + metadata.update({"feeds": feeds_and_data}) + self._post(path, metadata) else: self._post(path, {"feeds": feeds_and_data}) From 6e630a8f8c412a46dfa2c3b935777561fab908fd Mon Sep 17 00:00:00 2001 From: Tyeth Gundry Date: Tue, 16 Jul 2024 18:50:38 +0100 Subject: [PATCH 05/12] Update groups http example --- .../adafruit_io_http/adafruit_io_groups.py | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/examples/adafruit_io_http/adafruit_io_groups.py b/examples/adafruit_io_http/adafruit_io_groups.py index 06ac1f7..16cedd6 100644 --- a/examples/adafruit_io_http/adafruit_io_groups.py +++ b/examples/adafruit_io_http/adafruit_io_groups.py @@ -4,7 +4,7 @@ # Adafruit IO HTTP API - Group Interactions # Documentation: https://io.adafruit.com/api/docs/#groups # adafruit_circuitpython_adafruitio with an esp32spi_socket -import datetime +import adafruit_datetime as datetime import board import busio from digitalio import DigitalInOut @@ -15,14 +15,25 @@ # Add a secrets.py to your filesystem that has a dictionary called secrets with "ssid" and -# "password" keys with your WiFi credentials. DO NOT share that file or commit it into Git or other -# source control. +# "password" keys with your WiFi credentials, along with "aio_username" and "aio_key" for +# your Adafruit IO user/key. DO NOT share that file or commit it into Git or other source control. # pylint: disable=no-name-in-module,wrong-import-order try: from secrets import secrets except ImportError: - print("WiFi secrets are kept in secrets.py, please add them there!") - raise + import os + if os.getenv("ADAFRUIT_AIO_USERNAME") and os.getenv("ADAFRUIT_AIO_KEY"): + secrets = { + "aio_username": os.getenv("ADAFRUIT_AIO_USERNAME", "Your_Username_Here"), + "aio_key": os.getenv("ADAFRUIT_AIO_KEY", "Your_Adafruit_IO_Key_Here"), + "ssid": os.getenv("CIRCUITPY_WIFI_SSID", ""), + "password": os.getenv("CIRCUITPY_WIFI_PASSWORD", ""), + } + else: + print( + "WiFi + Adafruit IO secrets are kept in secrets.py, please add them there!" + ) + raise # If you are using a board with pre-defined ESP32 Pins: esp32_cs = DigitalInOut(board.ESP_CS) @@ -46,6 +57,11 @@ continue print("Connected to", str(esp.ssid, "utf-8"), "\tRSSI:", esp.rssi) +# If you are using a wifi based mcu use this instead of esp code above, remove the from +# adafruit_esp32spi import line, optionally esp.connect(secrets["ssid"], secrets["password"]) +# import wifi +# esp = wifi.radio + # Initialize a requests session pool = adafruit_connection_manager.get_radio_socketpool(esp) ssl_context = adafruit_connection_manager.get_radio_ssl_context(esp) @@ -84,9 +100,17 @@ print("Getting fresh humidity feed info... (notice groups)") print(io.get_feed(humidity_feed["key"])) +# fetch current time +print("Fetching current time from IO... ", end="") +year, month, day, hour, minute, second, *_ = io.receive_time() +old_time = datetime.datetime(year, month, day, hour, minute, second) +print(old_time.isoformat()) + # Publish data for multiple feeds to a group, use different timestamps for no reason print("Publishing batch data to group feeds with created_at set 99minutes ago...") -thetime = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(minutes=99) +thetime = old_time - datetime.timedelta(minutes=99) +print(thetime) + io.send_group_data( group_key=sensor_group["key"], feeds_and_data=[ From 55e0adb4b7c8782d095a9e6a3f41102c4c3ff5f0 Mon Sep 17 00:00:00 2001 From: Tyeth Gundry Date: Tue, 16 Jul 2024 18:55:07 +0100 Subject: [PATCH 06/12] formatting --- examples/adafruit_io_http/adafruit_io_groups.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/adafruit_io_http/adafruit_io_groups.py b/examples/adafruit_io_http/adafruit_io_groups.py index 16cedd6..5c4326f 100644 --- a/examples/adafruit_io_http/adafruit_io_groups.py +++ b/examples/adafruit_io_http/adafruit_io_groups.py @@ -22,6 +22,7 @@ from secrets import secrets except ImportError: import os + if os.getenv("ADAFRUIT_AIO_USERNAME") and os.getenv("ADAFRUIT_AIO_KEY"): secrets = { "aio_username": os.getenv("ADAFRUIT_AIO_USERNAME", "Your_Username_Here"), From 3e231a1b2fde125af309ef1a7ba6a88454862f47 Mon Sep 17 00:00:00 2001 From: Tyeth Gundry Date: Tue, 16 Jul 2024 19:02:17 +0100 Subject: [PATCH 07/12] Correct secrets key to ADAFRUIT_AIO_USERNAME --- examples/adafruit_io_http/adafruit_io_groups.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/adafruit_io_http/adafruit_io_groups.py b/examples/adafruit_io_http/adafruit_io_groups.py index 5c4326f..a7f0809 100644 --- a/examples/adafruit_io_http/adafruit_io_groups.py +++ b/examples/adafruit_io_http/adafruit_io_groups.py @@ -71,7 +71,10 @@ # If you are testing on python with blinka, use real requests below and comment out above: # import os, datetime, requests as real_requests # from adafruit_io.adafruit_io import IO_HTTP -# secrets = {"aio_username": os.getenv("AIO_USERNAME"), "aio_key": os.getenv("AIO_KEY")} +# secrets = { +# "aio_username": os.getenv("ADAFRUIT_AIO_USERNAME"), +# "aio_key": os.getenv("ADAFRUIT_AIO_KEY"), +# } # requests = real_requests.Session() From 435dab5e8d245196be82af2d465cc23f765dae13 Mon Sep 17 00:00:00 2001 From: tyeth Date: Tue, 16 Jul 2024 19:17:06 +0100 Subject: [PATCH 08/12] Add tz arg (TimeZone) to receive_time --- adafruit_io/adafruit_io.py | 6 +++++- examples/adafruit_io_http/adafruit_io_groups.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/adafruit_io/adafruit_io.py b/adafruit_io/adafruit_io.py index 085273c..ed1fc92 100755 --- a/adafruit_io/adafruit_io.py +++ b/adafruit_io/adafruit_io.py @@ -842,12 +842,16 @@ def receive_random_data(self, generator_id: int): path = self._compose_path("integrations/words/{0}".format(generator_id)) return self._get(path) - def receive_time(self): + def receive_time(self, tz: str = None): """ Returns a struct_time from the Adafruit IO Server based on the device's IP address. https://circuitpython.readthedocs.io/en/latest/shared-bindings/time/__init__.html#time.struct_time + + :param str tz: Timezone to return the time in, see https://io.adafruit.com/services/time """ path = self._compose_path("integrations/time/struct.json") + if tz is not None: + path += "?tz={0}".format(tz) time_struct = self._get(path) return time.struct_time( ( diff --git a/examples/adafruit_io_http/adafruit_io_groups.py b/examples/adafruit_io_http/adafruit_io_groups.py index a7f0809..12ab0d6 100644 --- a/examples/adafruit_io_http/adafruit_io_groups.py +++ b/examples/adafruit_io_http/adafruit_io_groups.py @@ -106,7 +106,7 @@ # fetch current time print("Fetching current time from IO... ", end="") -year, month, day, hour, minute, second, *_ = io.receive_time() +year, month, day, hour, minute, second, *_ = io.receive_time(tz="UTC") old_time = datetime.datetime(year, month, day, hour, minute, second) print(old_time.isoformat()) From 2ebf075406735c2bf0324013980e0e5604852541 Mon Sep 17 00:00:00 2001 From: tyeth Date: Tue, 16 Jul 2024 19:22:27 +0100 Subject: [PATCH 09/12] improve docstring and link tz list --- adafruit_io/adafruit_io.py | 1 + 1 file changed, 1 insertion(+) diff --git a/adafruit_io/adafruit_io.py b/adafruit_io/adafruit_io.py index ed1fc92..b185465 100755 --- a/adafruit_io/adafruit_io.py +++ b/adafruit_io/adafruit_io.py @@ -848,6 +848,7 @@ def receive_time(self, tz: str = None): https://circuitpython.readthedocs.io/en/latest/shared-bindings/time/__init__.html#time.struct_time :param str tz: Timezone to return the time in, see https://io.adafruit.com/services/time + default is based on the device's IP address being geolocated, falling back to UTC. """ path = self._compose_path("integrations/time/struct.json") if tz is not None: From 479afd4e3ac9bd52a78617d4204f2381e7a116f2 Mon Sep 17 00:00:00 2001 From: tyeth Date: Tue, 16 Jul 2024 19:24:53 +0100 Subject: [PATCH 10/12] Update argument name from tz to timezone --- adafruit_io/adafruit_io.py | 8 ++++---- examples/adafruit_io_http/adafruit_io_groups.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/adafruit_io/adafruit_io.py b/adafruit_io/adafruit_io.py index b185465..5bf34dc 100755 --- a/adafruit_io/adafruit_io.py +++ b/adafruit_io/adafruit_io.py @@ -842,17 +842,17 @@ def receive_random_data(self, generator_id: int): path = self._compose_path("integrations/words/{0}".format(generator_id)) return self._get(path) - def receive_time(self, tz: str = None): + def receive_time(self, timezone: str = None): """ Returns a struct_time from the Adafruit IO Server based on the device's IP address. https://circuitpython.readthedocs.io/en/latest/shared-bindings/time/__init__.html#time.struct_time - :param str tz: Timezone to return the time in, see https://io.adafruit.com/services/time + :param str timezone: Timezone to return the time in, see https://io.adafruit.com/services/time default is based on the device's IP address being geolocated, falling back to UTC. """ path = self._compose_path("integrations/time/struct.json") - if tz is not None: - path += "?tz={0}".format(tz) + if timezone is not None: + path += "?tz={0}".format(timezone) time_struct = self._get(path) return time.struct_time( ( diff --git a/examples/adafruit_io_http/adafruit_io_groups.py b/examples/adafruit_io_http/adafruit_io_groups.py index 12ab0d6..2e1754b 100644 --- a/examples/adafruit_io_http/adafruit_io_groups.py +++ b/examples/adafruit_io_http/adafruit_io_groups.py @@ -106,7 +106,7 @@ # fetch current time print("Fetching current time from IO... ", end="") -year, month, day, hour, minute, second, *_ = io.receive_time(tz="UTC") +year, month, day, hour, minute, second, *_ = io.receive_time(timezone="UTC") old_time = datetime.datetime(year, month, day, hour, minute, second) print(old_time.isoformat()) From 89765af9ca4e99cf65526611d58c30707e5b3520 Mon Sep 17 00:00:00 2001 From: tyeth Date: Tue, 16 Jul 2024 19:28:38 +0100 Subject: [PATCH 11/12] satisfy pylint --- adafruit_io/adafruit_io.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/adafruit_io/adafruit_io.py b/adafruit_io/adafruit_io.py index 5bf34dc..308f5e9 100755 --- a/adafruit_io/adafruit_io.py +++ b/adafruit_io/adafruit_io.py @@ -847,7 +847,7 @@ def receive_time(self, timezone: str = None): Returns a struct_time from the Adafruit IO Server based on the device's IP address. https://circuitpython.readthedocs.io/en/latest/shared-bindings/time/__init__.html#time.struct_time - :param str timezone: Timezone to return the time in, see https://io.adafruit.com/services/time + :param str timezone: Timezone to return time in, see https://io.adafruit.com/services/time default is based on the device's IP address being geolocated, falling back to UTC. """ path = self._compose_path("integrations/time/struct.json") @@ -855,6 +855,7 @@ def receive_time(self, timezone: str = None): path += "?tz={0}".format(timezone) time_struct = self._get(path) return time.struct_time( + # pylint: disable=line-too-long ( time_struct["year"], time_struct["mon"], From 7d7485032f1f81d70dfebdd79d7144ec4de6941c Mon Sep 17 00:00:00 2001 From: Tyeth Gundry Date: Tue, 16 Jul 2024 19:34:38 +0100 Subject: [PATCH 12/12] Cleanup timezone doctstring for pylint --- adafruit_io/adafruit_io.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/adafruit_io/adafruit_io.py b/adafruit_io/adafruit_io.py index 308f5e9..d780c6f 100755 --- a/adafruit_io/adafruit_io.py +++ b/adafruit_io/adafruit_io.py @@ -846,9 +846,10 @@ def receive_time(self, timezone: str = None): """ Returns a struct_time from the Adafruit IO Server based on the device's IP address. https://circuitpython.readthedocs.io/en/latest/shared-bindings/time/__init__.html#time.struct_time + The default time returned is based on the device's IP address being geolocated, + falling back to UTC if unable to be geolocated. The timezone can be manually set. :param str timezone: Timezone to return time in, see https://io.adafruit.com/services/time - default is based on the device's IP address being geolocated, falling back to UTC. """ path = self._compose_path("integrations/time/struct.json") if timezone is not None: