-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathadafruit_ble_apple_notification_center.py
283 lines (229 loc) · 9.63 KB
/
adafruit_ble_apple_notification_center.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# SPDX-FileCopyrightText: 2019 Scott Shawcroft for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
`adafruit_ble_apple_notification_center`
================================================================================
BLE library for the Apple Notification Center
* Author(s): Scott Shawcroft
**Software and Dependencies:**
* Adafruit CircuitPython (5.0.0-beta.2+) firmware for the supported boards:
https://github.com/adafruit/circuitpython/releases
* Adafruit's BLE library: https://github.com/adafruit/Adafruit_CircuitPython_BLE
"""
from __future__ import annotations
import struct
import time
try:
from typing import Generator, Union, Dict, Optional, Any
except ImportError:
pass
from adafruit_ble.services import Service
from adafruit_ble.uuid import VendorUUID
from adafruit_ble.characteristics.stream import StreamIn, StreamOut
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BLE_Apple_Notification_Center.git"
class _NotificationAttribute:
def __init__(self, attribute_id: int, *, max_length: bool = False) -> None:
self._id = attribute_id
self._max_length = max_length
def __get__(self, notification: Notification, cls: Any) -> str:
if self._id in notification._attribute_cache:
return notification._attribute_cache[self._id]
if self._max_length:
command = struct.pack("<BIBH", 0, notification.id, self._id, 255)
else:
command = struct.pack("<BIB", 0, notification.id, self._id)
notification.control_point.write(command)
while notification.data_source.in_waiting == 0:
pass
_, _ = struct.unpack("<BI", notification.data_source.read(5))
attribute_id, attribute_length = struct.unpack(
"<BH", notification.data_source.read(3)
)
if attribute_id != self._id:
raise RuntimeError("Data for other attribute")
value = notification.data_source.read(attribute_length)
value = value.decode("utf-8")
notification._attribute_cache[self._id] = value
return value
NOTIFICATION_CATEGORIES = (
"Other",
"IncomingCall",
"MissedCall",
"Voicemail",
"Social",
"Schedule",
"Email",
"News",
"HealthAndFitness",
"BusinessAndFinance",
"Location",
"Entertainment",
"ActiveCall",
)
class Notification:
"""One notification that appears in the iOS notification center."""
# pylint: disable=too-many-instance-attributes
app_id = _NotificationAttribute(0)
"""String id of the app that generated the notification. It is not the name of the app. For
example, Slack is "com.tinyspeck.chatlyio" and Twitter is "com.atebits.Tweetie2"."""
title = _NotificationAttribute(1, max_length=True)
"""Title of the notification. Varies per app."""
subtitle = _NotificationAttribute(2, max_length=True)
"""Subtitle of the notification. Varies per app."""
message = _NotificationAttribute(3, max_length=True)
"""Message body of the notification. Varies per app."""
message_size = _NotificationAttribute(4)
"""Total length of the message string."""
_raw_date = _NotificationAttribute(5)
positive_action_label = _NotificationAttribute(6)
"""Human readable label of the positive action."""
negative_action_label = _NotificationAttribute(7)
"""Human readable label of the negative action."""
def __init__(
self,
notification_id: int,
event_flags: int,
category_id: int,
category_count: int,
*,
control_point: StreamIn,
data_source: StreamOut,
) -> None:
self.id = notification_id # pylint: disable=invalid-name
"""Integer id of the notification."""
self.removed = False
"""True when the notification has been cleared on the iOS device."""
self.silent = False
self.important = False
self.preexisting = False
"""True if the notification existed before we connected to the iOS device."""
self.positive_action = False
"""True if the notification has a positive action to respond with. For example, this could
be answering a phone call."""
self.negative_action = False
"""True if the notification has a negative action to respond with. For example, this could
be declining a phone call."""
self.category_count = 0
"""Number of other notifications with the same category."""
self.update(event_flags, category_id, category_count)
self._attribute_cache: Dict[int, str] = {}
self.control_point = control_point
self.data_source = data_source
def send_positive_action(self) -> None:
"""Sends positive action on this notification. For example, to accept an IncomingCall."""
cmd = 2 # ANCS_CMD_PERFORM_NOTIFICATION_ACTION,
uid = self.id
action_id = 0 # ANCS_ACTION_POSITIVE
buffer = struct.pack("<BIB", cmd, uid, action_id)
self.control_point.write(buffer)
def send_negative_action(self) -> None:
"""Sends negative action on this notification. For example, to decline an IncomingCall."""
cmd = 2 # ANCS_CMD_PERFORM_NOTIFICATION_ACTION,
uid = self.id
action_id = 1 # ANCS_ACTION_NEGATIVE
buffer = struct.pack("<BIB", cmd, uid, action_id)
self.control_point.write(buffer)
def update(self, event_flags: int, category_id: int, category_count: int) -> None:
"""Update the notification and clear the attribute cache."""
self.category_id = category_id
self.category_count = category_count
self.silent = (event_flags & (1 << 0)) != 0
self.important = (event_flags & (1 << 1)) != 0
self.preexisting = (event_flags & (1 << 2)) != 0
self.positive_action = (event_flags & (1 << 3)) != 0
self.negative_action = (event_flags & (1 << 4)) != 0
self._attribute_cache = {}
def __str__(self) -> str:
# pylint: disable=too-many-branches
flags = []
category = None
if self.category_id < len(NOTIFICATION_CATEGORIES):
category = NOTIFICATION_CATEGORIES[self.category_id]
else:
category = "Reserved"
if self.silent:
flags.append("silent")
if self.important:
flags.append("important")
if self.preexisting:
flags.append("preexisting")
if self.positive_action:
flags.append("positive_action")
if self.negative_action:
flags.append("negative_action")
return (
category
+ " "
+ " ".join(flags)
+ " "
+ self.app_id
+ " "
+ str(self.title)
+ " "
+ str(self.subtitle)
+ " "
+ str(self.message)
)
class AppleNotificationCenterService(Service):
"""Notification service.
Documented by Apple here:
https://developer.apple.com/library/archive/documentation/CoreBluetooth/Reference/AppleNotificationCenterServiceSpecification/Specification/Specification.html
"""
uuid = VendorUUID("7905F431-B5CE-4E99-A40F-4B1E122D00D0")
control_point = StreamIn(uuid=VendorUUID("69D1D8F3-45E1-49A8-9821-9BBDFDAAD9D9"))
data_source = StreamOut(
uuid=VendorUUID("22EAC6E9-24D6-4BB5-BE44-B36ACE7C7BFB"), buffer_size=1024
)
notification_source = StreamOut(
uuid=VendorUUID("9FBF120D-6301-42D9-8C58-25E699A21DBD"), buffer_size=8 * 100
)
def __init__(self, service: Service = None) -> None:
super().__init__(service=service)
self._active_notifications: Dict[tuple, Notification] = {}
def _update(self) -> Generator[Union[Notification, None], None, None]:
# Pylint is incorrectly inferring the type of self.notification_source so disable no-member.
while self.notification_source.in_waiting > 7: # pylint: disable=no-member
buffer = self.notification_source.read(8) # pylint: disable=no-member
event_id, event_flags, category_id, category_count, nid = struct.unpack(
"<BBBBI", buffer
)
if event_id == 0:
self._active_notifications[nid] = Notification(
nid,
event_flags,
category_id,
category_count,
control_point=self.control_point,
data_source=self.data_source,
)
yield self._active_notifications[nid]
elif event_id == 1:
self._active_notifications[nid].update(
event_flags, category_id, category_count
)
yield None
elif event_id == 2:
self._active_notifications[nid].removed = True
del self._active_notifications[nid]
yield None
def wait_for_new_notifications(
self, timeout: Optional[float] = None
) -> Generator[Union[Notification, None], None, None]:
"""Waits for new notifications and yields them. Returns on timeout, update, disconnect or
clear."""
start_time = time.monotonic()
while timeout is None or timeout > time.monotonic() - start_time:
try:
new_notification = next(self._update())
except StopIteration:
return
if new_notification:
yield new_notification
@property
def active_notifications(self) -> dict:
"""A dictionary of active notifications keyed by id."""
for _ in self._update():
pass
return self._active_notifications