/ examples / ble_broadcastnet_blinka_bridge.py
ble_broadcastnet_blinka_bridge.py
  1  """This example bridges from BLE to Adafruit IO on a Raspberry Pi."""
  2  from secrets import secrets  # pylint: disable=no-name-in-module
  3  import time
  4  import requests
  5  from adafruit_ble.advertising.standard import ManufacturerDataField
  6  import adafruit_ble
  7  import adafruit_ble_broadcastnet
  8  
  9  aio_auth_header = {"X-AIO-KEY": secrets["aio_key"]}
 10  aio_base_url = "https://io.adafruit.com/api/v2/" + secrets["aio_username"]
 11  
 12  
 13  def aio_post(path, **kwargs):
 14      kwargs["headers"] = aio_auth_header
 15      return requests.post(aio_base_url + path, **kwargs)
 16  
 17  
 18  def aio_get(path, **kwargs):
 19      kwargs["headers"] = aio_auth_header
 20      return requests.get(aio_base_url + path, **kwargs)
 21  
 22  
 23  # Disable outer names check because we frequently collide.
 24  # pylint: disable=redefined-outer-name
 25  
 26  
 27  def create_group(name):
 28      response = aio_post("/groups", json={"name": name})
 29      if response.status_code != 201:
 30          print(name)
 31          print(response.content)
 32          print(response.status_code)
 33          raise RuntimeError("unable to create new group")
 34      return response.json()["key"]
 35  
 36  
 37  def create_feed(group_key, name):
 38      response = aio_post(
 39          "/groups/{}/feeds".format(group_key), json={"feed": {"name": name}}
 40      )
 41      if response.status_code != 201:
 42          print(name)
 43          print(response.content)
 44          print(response.status_code)
 45          raise RuntimeError("unable to create new feed")
 46      return response.json()["key"]
 47  
 48  
 49  def create_data(group_key, data):
 50      response = aio_post("/groups/{}/data".format(group_key), json={"feeds": data})
 51      if response.status_code == 429:
 52          print("Throttled!")
 53          return False
 54      if response.status_code != 200:
 55          print(response.status_code, response.json())
 56          raise RuntimeError("unable to create new data")
 57      response.close()
 58      return True
 59  
 60  
 61  def convert_to_feed_data(values, attribute_name, attribute_instance):
 62      feed_data = []
 63      # Wrap single value entries for enumeration.
 64      if not isinstance(values, tuple) or (
 65          attribute_instance.element_count > 1 and not isinstance(values[0], tuple)
 66      ):
 67          values = (values,)
 68      for i, value in enumerate(values):
 69          key = attribute_name.replace("_", "-") + "-" + str(i)
 70          if isinstance(value, tuple):
 71              for j in range(attribute_instance.element_count):
 72                  feed_data.append(
 73                      {
 74                          "key": key + "-" + attribute_instance.field_names[j],
 75                          "value": value[j],
 76                      }
 77                  )
 78          else:
 79              feed_data.append({"key": key, "value": value})
 80      return feed_data
 81  
 82  
 83  ble = adafruit_ble.BLERadio()
 84  bridge_address = adafruit_ble_broadcastnet.device_address
 85  print("This is BroadcastNet bridge:", bridge_address)
 86  print()
 87  
 88  print("Fetching existing feeds.")
 89  
 90  existing_feeds = {}
 91  response = aio_get("/groups")
 92  for group in response.json():
 93      if "-" not in group["key"]:
 94          continue
 95      pieces = group["key"].split("-")
 96      if len(pieces) != 4 or pieces[0] != "bridge" or pieces[2] != "sensor":
 97          continue
 98      _, bridge, _, sensor_address = pieces
 99      if bridge != bridge_address:
100          continue
101      existing_feeds[sensor_address] = []
102      for feed in group["feeds"]:
103          feed_key = feed["key"].split(".")[-1]
104          existing_feeds[sensor_address].append(feed_key)
105  
106  print(existing_feeds)
107  
108  print("scanning")
109  print()
110  sequence_numbers = {}
111  # By providing Advertisement as well we include everything, not just specific advertisements.
112  for measurement in ble.start_scan(
113      adafruit_ble_broadcastnet.AdafruitSensorMeasurement, interval=0.5
114  ):
115      reversed_address = [measurement.address.address_bytes[i] for i in range(5, -1, -1)]
116      sensor_address = "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}".format(*reversed_address)
117      if sensor_address not in sequence_numbers:
118          sequence_numbers[sensor_address] = measurement.sequence_number - 1 % 256
119      # Skip if we are getting the same broadcast more than once.
120      if measurement.sequence_number == sequence_numbers[sensor_address]:
121          continue
122      number_missed = measurement.sequence_number - sequence_numbers[sensor_address] - 1
123      if number_missed < 0:
124          number_missed += 256
125      group_key = "bridge-{}-sensor-{}".format(bridge_address, sensor_address)
126      if sensor_address not in existing_feeds:
127          create_group("Bridge {} Sensor {}".format(bridge_address, sensor_address))
128          create_feed(group_key, "Missed Message Count")
129          existing_feeds[sensor_address] = ["missed-message-count"]
130  
131      data = [{"key": "missed-message-count", "value": number_missed}]
132      for attribute in dir(measurement.__class__):
133          attribute_instance = getattr(measurement.__class__, attribute)
134          if issubclass(attribute_instance.__class__, ManufacturerDataField):
135              if attribute != "sequence_number":
136                  values = getattr(measurement, attribute)
137                  if values is not None:
138                      data.extend(
139                          convert_to_feed_data(values, attribute, attribute_instance)
140                      )
141  
142      for feed_data in data:
143          if feed_data["key"] not in existing_feeds[sensor_address]:
144              create_feed(group_key, feed_data["key"])
145              existing_feeds[sensor_address].append(feed_data["key"])
146  
147      start_time = time.monotonic()
148      print(group_key, data)
149      # Only update the previous sequence if we logged successfully.
150      if create_data(group_key, data):
151          sequence_numbers[sensor_address] = measurement.sequence_number
152  
153      duration = time.monotonic() - start_time
154      print("Done logging measurement to IO. Took {} seconds".format(duration))
155      print()
156  
157  print("scan done")