/ adafruit_ble_apple_notification_center.py
adafruit_ble_apple_notification_center.py
  1  # The MIT License (MIT)
  2  #
  3  # Copyright (c) 2019 Scott Shawcroft for Adafruit Industries LLC
  4  #
  5  # Permission is hereby granted, free of charge, to any person obtaining a copy
  6  # of this software and associated documentation files (the "Software"), to deal
  7  # in the Software without restriction, including without limitation the rights
  8  # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9  # copies of the Software, and to permit persons to whom the Software is
 10  # furnished to do so, subject to the following conditions:
 11  #
 12  # The above copyright notice and this permission notice shall be included in
 13  # all copies or substantial portions of the Software.
 14  #
 15  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 16  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 17  # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 18  # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 19  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 20  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 21  # THE SOFTWARE.
 22  """
 23  `adafruit_ble_apple_notification_center`
 24  ================================================================================
 25  
 26  BLE library for the Apple Notification Center
 27  
 28  * Author(s): Scott Shawcroft
 29  
 30  **Software and Dependencies:**
 31  
 32  * Adafruit CircuitPython (5.0.0-beta.2+) firmware for the supported boards:
 33    https://github.com/adafruit/circuitpython/releases
 34  * Adafruit's BLE library: https://github.com/adafruit/Adafruit_CircuitPython_BLE
 35  """
 36  
 37  import struct
 38  import time
 39  
 40  from adafruit_ble.services import Service
 41  from adafruit_ble.uuid import VendorUUID
 42  from adafruit_ble.characteristics.stream import StreamIn, StreamOut
 43  
 44  __version__ = "0.0.0-auto.0"
 45  __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE_Apple_Notification_Center.git"
 46  
 47  
 48  class _NotificationAttribute:
 49      def __init__(self, attribute_id, *, max_length=False):
 50          self._id = attribute_id
 51          self._max_length = max_length
 52  
 53      def __get__(self, notification, cls):
 54          if self._id in notification._attribute_cache:
 55              return notification._attribute_cache[self._id]
 56  
 57          if self._max_length:
 58              command = struct.pack("<BIBH", 0, notification.id, self._id, 255)
 59          else:
 60              command = struct.pack("<BIB", 0, notification.id, self._id)
 61          notification.control_point.write(command)
 62          while notification.data_source.in_waiting == 0:
 63              pass
 64  
 65          _, _ = struct.unpack("<BI", notification.data_source.read(5))
 66          attribute_id, attribute_length = struct.unpack(
 67              "<BH", notification.data_source.read(3)
 68          )
 69          if attribute_id != self._id:
 70              raise RuntimeError("Data for other attribute")
 71          value = notification.data_source.read(attribute_length)
 72          value = value.decode("utf-8")
 73          notification._attribute_cache[self._id] = value
 74          return value
 75  
 76  
 77  NOTIFICATION_CATEGORIES = (
 78      "Other",
 79      "IncomingCall",
 80      "MissedCall",
 81      "Voicemail",
 82      "Social",
 83      "Schedule",
 84      "Email",
 85      "News",
 86      "HealthAndFitness",
 87      "BusinessAndFinance",
 88      "Location",
 89      "Entertainment",
 90  )
 91  
 92  
 93  class Notification:
 94      """One notification that appears in the iOS notification center."""
 95  
 96      # pylint: disable=too-many-instance-attributes
 97  
 98      app_id = _NotificationAttribute(0)
 99      """String id of the app that generated the notification. It is not the name of the app. For
100         example, Slack is "com.tinyspeck.chatlyio" and Twitter is "com.atebits.Tweetie2"."""
101  
102      title = _NotificationAttribute(1, max_length=True)
103      """Title of the notification. Varies per app."""
104  
105      subtitle = _NotificationAttribute(2, max_length=True)
106      """Subtitle of the notification. Varies per app."""
107  
108      message = _NotificationAttribute(3, max_length=True)
109      """Message body of the notification. Varies per app."""
110  
111      message_size = _NotificationAttribute(4)
112      """Total length of the message string."""
113  
114      _raw_date = _NotificationAttribute(5)
115      positive_action_label = _NotificationAttribute(6)
116      """Human readable label of the positive action."""
117  
118      negative_action_label = _NotificationAttribute(7)
119      """Human readable label of the negative action."""
120  
121      def __init__(
122          self,
123          notification_id,
124          event_flags,
125          category_id,
126          category_count,
127          *,
128          control_point,
129          data_source
130      ):
131          self.id = notification_id  # pylint: disable=invalid-name
132          """Integer id of the notification."""
133  
134          self.removed = False
135          """True when the notification has been cleared on the iOS device."""
136  
137          self.silent = False
138          self.important = False
139          self.preexisting = False
140          """True if the notification existed before we connected to the iOS device."""
141  
142          self.positive_action = False
143          """True if the notification has a positive action to respond with. For example, this could
144             be answering a phone call."""
145  
146          self.negative_action = False
147          """True if the notification has a negative action to respond with. For example, this could
148             be declining a phone call."""
149  
150          self.category_count = 0
151          """Number of other notifications with the same category."""
152  
153          self.update(event_flags, category_id, category_count)
154  
155          self._attribute_cache = {}
156  
157          self.control_point = control_point
158          self.data_source = data_source
159  
160      def update(self, event_flags, category_id, category_count):
161          """Update the notification and clear the attribute cache."""
162          self.category_id = category_id
163  
164          self.category_count = category_count
165  
166          self.silent = (event_flags & (1 << 0)) != 0
167          self.important = (event_flags & (1 << 1)) != 0
168          self.preexisting = (event_flags & (1 << 2)) != 0
169          self.positive_action = (event_flags & (1 << 3)) != 0
170          self.negative_action = (event_flags & (1 << 4)) != 0
171  
172          self._attribute_cache = {}
173  
174      def __str__(self):
175          # pylint: disable=too-many-branches
176          flags = []
177          category = None
178          if self.category_id < len(NOTIFICATION_CATEGORIES):
179              category = NOTIFICATION_CATEGORIES[self.category_id]
180          else:
181              category = "Reserved"
182  
183          if self.silent:
184              flags.append("silent")
185          if self.important:
186              flags.append("important")
187          if self.preexisting:
188              flags.append("preexisting")
189          if self.positive_action:
190              flags.append("positive_action")
191          if self.negative_action:
192              flags.append("negative_action")
193          return (
194              category
195              + " "
196              + " ".join(flags)
197              + " "
198              + self.app_id
199              + " "
200              + str(self.title)
201              + " "
202              + str(self.subtitle)
203              + " "
204              + str(self.message)
205          )
206  
207  
208  class AppleNotificationCenterService(Service):
209      """Notification service.
210  
211      Documented by Apple here:
212        https://developer.apple.com/library/archive/documentation/CoreBluetooth/Reference/AppleNotificationCenterServiceSpecification/Specification/Specification.html
213  
214      """
215  
216      uuid = VendorUUID("7905F431-B5CE-4E99-A40F-4B1E122D00D0")
217  
218      control_point = StreamIn(uuid=VendorUUID("69D1D8F3-45E1-49A8-9821-9BBDFDAAD9D9"))
219      data_source = StreamOut(
220          uuid=VendorUUID("22EAC6E9-24D6-4BB5-BE44-B36ACE7C7BFB"), buffer_size=1024
221      )
222      notification_source = StreamOut(
223          uuid=VendorUUID("9FBF120D-6301-42D9-8C58-25E699A21DBD"), buffer_size=8 * 100
224      )
225  
226      def __init__(self, service=None):
227          super().__init__(service=service)
228          self._active_notifications = {}
229  
230      def _update(self):
231          # Pylint is incorrectly inferring the type of self.notification_source so disable no-member.
232          while self.notification_source.in_waiting > 7:  # pylint: disable=no-member
233              buffer = self.notification_source.read(8)  # pylint: disable=no-member
234              event_id, event_flags, category_id, category_count, nid = struct.unpack(
235                  "<BBBBI", buffer
236              )
237              if event_id == 0:
238                  self._active_notifications[nid] = Notification(
239                      nid,
240                      event_flags,
241                      category_id,
242                      category_count,
243                      control_point=self.control_point,
244                      data_source=self.data_source,
245                  )
246                  yield self._active_notifications[nid]
247              elif event_id == 1:
248                  self._active_notifications[nid].update(
249                      event_flags, category_id, category_count
250                  )
251                  yield None
252              elif event_id == 2:
253                  self._active_notifications[nid].removed = True
254                  del self._active_notifications[nid]
255                  yield None
256  
257      def wait_for_new_notifications(self, timeout=None):
258          """Waits for new notifications and yields them. Returns on timeout, update, disconnect or
259             clear."""
260          start_time = time.monotonic()
261          while timeout is None or timeout > time.monotonic() - start_time:
262              try:
263                  new_notification = next(self._update())
264              except StopIteration:
265                  return
266              if new_notification:
267                  yield new_notification
268  
269      @property
270      def active_notifications(self):
271          """A dictionary of active notifications keyed by id."""
272          for _ in self._update():
273              pass
274          return self._active_notifications