/ 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