bumble 0.0.204__py3-none-any.whl → 0.0.208__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. bumble/_version.py +9 -4
  2. bumble/apps/auracast.py +631 -98
  3. bumble/apps/bench.py +238 -157
  4. bumble/apps/console.py +19 -12
  5. bumble/apps/controller_info.py +23 -7
  6. bumble/apps/device_info.py +50 -4
  7. bumble/apps/gg_bridge.py +1 -1
  8. bumble/apps/lea_unicast/app.py +61 -201
  9. bumble/att.py +51 -37
  10. bumble/audio/__init__.py +17 -0
  11. bumble/audio/io.py +553 -0
  12. bumble/controller.py +24 -9
  13. bumble/core.py +305 -156
  14. bumble/device.py +1090 -99
  15. bumble/gatt.py +36 -226
  16. bumble/gatt_adapters.py +374 -0
  17. bumble/gatt_client.py +52 -33
  18. bumble/gatt_server.py +5 -5
  19. bumble/hci.py +812 -14
  20. bumble/host.py +367 -65
  21. bumble/l2cap.py +3 -16
  22. bumble/pairing.py +5 -5
  23. bumble/pandora/host.py +7 -12
  24. bumble/profiles/aics.py +48 -57
  25. bumble/profiles/ascs.py +8 -19
  26. bumble/profiles/asha.py +16 -14
  27. bumble/profiles/bass.py +16 -22
  28. bumble/profiles/battery_service.py +13 -3
  29. bumble/profiles/device_information_service.py +16 -14
  30. bumble/profiles/gap.py +12 -8
  31. bumble/profiles/gatt_service.py +167 -0
  32. bumble/profiles/gmap.py +198 -0
  33. bumble/profiles/hap.py +8 -6
  34. bumble/profiles/heart_rate_service.py +20 -4
  35. bumble/profiles/le_audio.py +87 -4
  36. bumble/profiles/mcp.py +11 -9
  37. bumble/profiles/pacs.py +61 -16
  38. bumble/profiles/tmap.py +8 -12
  39. bumble/profiles/{vcp.py → vcs.py} +35 -29
  40. bumble/profiles/vocs.py +62 -85
  41. bumble/sdp.py +223 -93
  42. bumble/smp.py +1 -1
  43. bumble/utils.py +12 -2
  44. bumble/vendor/android/hci.py +1 -1
  45. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/METADATA +13 -11
  46. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/RECORD +50 -46
  47. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/WHEEL +1 -1
  48. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/entry_points.txt +1 -0
  49. bumble/apps/lea_unicast/liblc3.wasm +0 -0
  50. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/LICENSE +0 -0
  51. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/top_level.txt +0 -0
bumble/profiles/pacs.py CHANGED
@@ -25,6 +25,7 @@ from typing import Optional, Sequence, Union
25
25
  from bumble.profiles.bap import AudioLocation, CodecSpecificCapabilities, ContextType
26
26
  from bumble.profiles import le_audio
27
27
  from bumble import gatt
28
+ from bumble import gatt_adapters
28
29
  from bumble import gatt_client
29
30
  from bumble import hci
30
31
 
@@ -72,6 +73,19 @@ class PacRecord:
72
73
  metadata=metadata,
73
74
  )
74
75
 
76
+ @classmethod
77
+ def list_from_bytes(cls, data: bytes) -> list[PacRecord]:
78
+ """Parse a serialized list of records preceded by a one byte list length."""
79
+ record_count = data[0]
80
+ records = []
81
+ offset = 1
82
+ for _ in range(record_count):
83
+ record = PacRecord.from_bytes(data[offset:])
84
+ offset += len(bytes(record))
85
+ records.append(record)
86
+
87
+ return records
88
+
75
89
  def __bytes__(self) -> bytes:
76
90
  capabilities_bytes = bytes(self.codec_specific_capabilities)
77
91
  metadata_bytes = bytes(self.metadata)
@@ -172,39 +186,70 @@ class PublishedAudioCapabilitiesService(gatt.TemplateService):
172
186
  class PublishedAudioCapabilitiesServiceProxy(gatt_client.ProfileServiceProxy):
173
187
  SERVICE_CLASS = PublishedAudioCapabilitiesService
174
188
 
175
- sink_pac: Optional[gatt_client.CharacteristicProxy] = None
176
- sink_audio_locations: Optional[gatt_client.CharacteristicProxy] = None
177
- source_pac: Optional[gatt_client.CharacteristicProxy] = None
178
- source_audio_locations: Optional[gatt_client.CharacteristicProxy] = None
179
- available_audio_contexts: gatt_client.CharacteristicProxy
180
- supported_audio_contexts: gatt_client.CharacteristicProxy
189
+ sink_pac: Optional[gatt_client.CharacteristicProxy[list[PacRecord]]] = None
190
+ sink_audio_locations: Optional[gatt_client.CharacteristicProxy[AudioLocation]] = (
191
+ None
192
+ )
193
+ source_pac: Optional[gatt_client.CharacteristicProxy[list[PacRecord]]] = None
194
+ source_audio_locations: Optional[gatt_client.CharacteristicProxy[AudioLocation]] = (
195
+ None
196
+ )
197
+ available_audio_contexts: gatt_client.CharacteristicProxy[tuple[ContextType, ...]]
198
+ supported_audio_contexts: gatt_client.CharacteristicProxy[tuple[ContextType, ...]]
181
199
 
182
200
  def __init__(self, service_proxy: gatt_client.ServiceProxy):
183
201
  self.service_proxy = service_proxy
184
202
 
185
- self.available_audio_contexts = service_proxy.get_characteristics_by_uuid(
186
- gatt.GATT_AVAILABLE_AUDIO_CONTEXTS_CHARACTERISTIC
187
- )[0]
188
- self.supported_audio_contexts = service_proxy.get_characteristics_by_uuid(
189
- gatt.GATT_SUPPORTED_AUDIO_CONTEXTS_CHARACTERISTIC
190
- )[0]
203
+ self.available_audio_contexts = (
204
+ gatt_adapters.DelegatedCharacteristicProxyAdapter(
205
+ service_proxy.get_required_characteristic_by_uuid(
206
+ gatt.GATT_AVAILABLE_AUDIO_CONTEXTS_CHARACTERISTIC
207
+ ),
208
+ decode=lambda x: tuple(map(ContextType, struct.unpack('<HH', x))),
209
+ )
210
+ )
211
+
212
+ self.supported_audio_contexts = (
213
+ gatt_adapters.DelegatedCharacteristicProxyAdapter(
214
+ service_proxy.get_required_characteristic_by_uuid(
215
+ gatt.GATT_SUPPORTED_AUDIO_CONTEXTS_CHARACTERISTIC
216
+ ),
217
+ decode=lambda x: tuple(map(ContextType, struct.unpack('<HH', x))),
218
+ )
219
+ )
191
220
 
192
221
  if characteristics := service_proxy.get_characteristics_by_uuid(
193
222
  gatt.GATT_SINK_PAC_CHARACTERISTIC
194
223
  ):
195
- self.sink_pac = characteristics[0]
224
+ self.sink_pac = gatt_adapters.DelegatedCharacteristicProxyAdapter(
225
+ characteristics[0],
226
+ decode=PacRecord.list_from_bytes,
227
+ )
196
228
 
197
229
  if characteristics := service_proxy.get_characteristics_by_uuid(
198
230
  gatt.GATT_SOURCE_PAC_CHARACTERISTIC
199
231
  ):
200
- self.source_pac = characteristics[0]
232
+ self.source_pac = gatt_adapters.DelegatedCharacteristicProxyAdapter(
233
+ characteristics[0],
234
+ decode=PacRecord.list_from_bytes,
235
+ )
201
236
 
202
237
  if characteristics := service_proxy.get_characteristics_by_uuid(
203
238
  gatt.GATT_SINK_AUDIO_LOCATION_CHARACTERISTIC
204
239
  ):
205
- self.sink_audio_locations = characteristics[0]
240
+ self.sink_audio_locations = (
241
+ gatt_adapters.DelegatedCharacteristicProxyAdapter(
242
+ characteristics[0],
243
+ decode=lambda x: AudioLocation(struct.unpack('<I', x)[0]),
244
+ )
245
+ )
206
246
 
207
247
  if characteristics := service_proxy.get_characteristics_by_uuid(
208
248
  gatt.GATT_SOURCE_AUDIO_LOCATION_CHARACTERISTIC
209
249
  ):
210
- self.source_audio_locations = characteristics[0]
250
+ self.source_audio_locations = (
251
+ gatt_adapters.DelegatedCharacteristicProxyAdapter(
252
+ characteristics[0],
253
+ decode=lambda x: AudioLocation(struct.unpack('<I', x)[0]),
254
+ )
255
+ )
bumble/profiles/tmap.py CHANGED
@@ -24,12 +24,11 @@ import struct
24
24
  from bumble.gatt import (
25
25
  TemplateService,
26
26
  Characteristic,
27
- DelegatedCharacteristicAdapter,
28
- InvalidServiceError,
29
27
  GATT_TELEPHONY_AND_MEDIA_AUDIO_SERVICE,
30
28
  GATT_TMAP_ROLE_CHARACTERISTIC,
31
29
  )
32
- from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
30
+ from bumble.gatt_adapters import DelegatedCharacteristicProxyAdapter
31
+ from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
33
32
 
34
33
 
35
34
  # -----------------------------------------------------------------------------
@@ -54,6 +53,8 @@ class Role(enum.IntFlag):
54
53
  class TelephonyAndMediaAudioService(TemplateService):
55
54
  UUID = GATT_TELEPHONY_AND_MEDIA_AUDIO_SERVICE
56
55
 
56
+ role_characteristic: Characteristic[bytes]
57
+
57
58
  def __init__(self, role: Role):
58
59
  self.role_characteristic = Characteristic(
59
60
  GATT_TMAP_ROLE_CHARACTERISTIC,
@@ -69,20 +70,15 @@ class TelephonyAndMediaAudioService(TemplateService):
69
70
  class TelephonyAndMediaAudioServiceProxy(ProfileServiceProxy):
70
71
  SERVICE_CLASS = TelephonyAndMediaAudioService
71
72
 
72
- role: DelegatedCharacteristicAdapter
73
+ role: CharacteristicProxy[Role]
73
74
 
74
75
  def __init__(self, service_proxy: ServiceProxy):
75
76
  self.service_proxy = service_proxy
76
77
 
77
- if not (
78
- characteristics := service_proxy.get_characteristics_by_uuid(
78
+ self.role = DelegatedCharacteristicProxyAdapter(
79
+ service_proxy.get_required_characteristic_by_uuid(
79
80
  GATT_TMAP_ROLE_CHARACTERISTIC
80
- )
81
- ):
82
- raise InvalidServiceError('TMAP Role characteristic not found')
83
-
84
- self.role = DelegatedCharacteristicAdapter(
85
- characteristics[0],
81
+ ),
86
82
  decode=lambda value: Role(
87
83
  struct.unpack_from('<H', value, 0)[0],
88
84
  ),
@@ -1,4 +1,4 @@
1
- # Copyright 2021-2024 Google LLC
1
+ # Copyright 2021-2025 Google LLC
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -17,14 +17,17 @@
17
17
  # Imports
18
18
  # -----------------------------------------------------------------------------
19
19
  from __future__ import annotations
20
+ import dataclasses
20
21
  import enum
21
22
 
23
+ from typing import Optional, Sequence
24
+
22
25
  from bumble import att
23
26
  from bumble import device
24
27
  from bumble import gatt
28
+ from bumble import gatt_adapters
25
29
  from bumble import gatt_client
26
30
 
27
- from typing import Optional, Sequence
28
31
 
29
32
  # -----------------------------------------------------------------------------
30
33
  # Constants
@@ -67,6 +70,20 @@ class VolumeControlPointOpcode(enum.IntEnum):
67
70
  MUTE = 0x06
68
71
 
69
72
 
73
+ @dataclasses.dataclass
74
+ class VolumeState:
75
+ volume_setting: int
76
+ mute: int
77
+ change_counter: int
78
+
79
+ @classmethod
80
+ def from_bytes(cls, data: bytes) -> VolumeState:
81
+ return cls(data[0], data[1], data[2])
82
+
83
+ def __bytes__(self) -> bytes:
84
+ return bytes([self.volume_setting, self.mute, self.change_counter])
85
+
86
+
70
87
  # -----------------------------------------------------------------------------
71
88
  # Server
72
89
  # -----------------------------------------------------------------------------
@@ -126,16 +143,8 @@ class VolumeControlService(gatt.TemplateService):
126
143
  included_services=list(included_services),
127
144
  )
128
145
 
129
- @property
130
- def volume_state_bytes(self) -> bytes:
131
- return bytes([self.volume_setting, self.muted, self.change_counter])
132
-
133
- @volume_state_bytes.setter
134
- def volume_state_bytes(self, new_value: bytes) -> None:
135
- self.volume_setting, self.muted, self.change_counter = new_value
136
-
137
146
  def _on_read_volume_state(self, _connection: Optional[device.Connection]) -> bytes:
138
- return self.volume_state_bytes
147
+ return bytes(VolumeState(self.volume_setting, self.muted, self.change_counter))
139
148
 
140
149
  def _on_write_volume_control_point(
141
150
  self, connection: Optional[device.Connection], value: bytes
@@ -153,14 +162,9 @@ class VolumeControlService(gatt.TemplateService):
153
162
  self.change_counter = (self.change_counter + 1) % 256
154
163
  connection.abort_on(
155
164
  'disconnection',
156
- connection.device.notify_subscribers(
157
- attribute=self.volume_state,
158
- value=self.volume_state_bytes,
159
- ),
160
- )
161
- self.emit(
162
- 'volume_state', self.volume_setting, self.muted, self.change_counter
165
+ connection.device.notify_subscribers(attribute=self.volume_state),
163
166
  )
167
+ self.emit('volume_state_change')
164
168
 
165
169
  def _on_relative_volume_down(self) -> bool:
166
170
  old_volume = self.volume_setting
@@ -206,25 +210,27 @@ class VolumeControlService(gatt.TemplateService):
206
210
  class VolumeControlServiceProxy(gatt_client.ProfileServiceProxy):
207
211
  SERVICE_CLASS = VolumeControlService
208
212
 
209
- volume_control_point: gatt_client.CharacteristicProxy
213
+ volume_control_point: gatt_client.CharacteristicProxy[bytes]
214
+ volume_state: gatt_client.CharacteristicProxy[VolumeState]
215
+ volume_flags: gatt_client.CharacteristicProxy[VolumeFlags]
210
216
 
211
217
  def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
212
218
  self.service_proxy = service_proxy
213
219
 
214
- self.volume_state = gatt.PackedCharacteristicAdapter(
215
- service_proxy.get_characteristics_by_uuid(
220
+ self.volume_state = gatt_adapters.SerializableCharacteristicProxyAdapter(
221
+ service_proxy.get_required_characteristic_by_uuid(
216
222
  gatt.GATT_VOLUME_STATE_CHARACTERISTIC
217
- )[0],
218
- 'BBB',
223
+ ),
224
+ VolumeState,
219
225
  )
220
226
 
221
- self.volume_control_point = service_proxy.get_characteristics_by_uuid(
227
+ self.volume_control_point = service_proxy.get_required_characteristic_by_uuid(
222
228
  gatt.GATT_VOLUME_CONTROL_POINT_CHARACTERISTIC
223
- )[0]
229
+ )
224
230
 
225
- self.volume_flags = gatt.PackedCharacteristicAdapter(
226
- service_proxy.get_characteristics_by_uuid(
231
+ self.volume_flags = gatt_adapters.DelegatedCharacteristicProxyAdapter(
232
+ service_proxy.get_required_characteristic_by_uuid(
227
233
  gatt.GATT_VOLUME_FLAGS_CHARACTERISTIC
228
- )[0],
229
- 'B',
234
+ ),
235
+ decode=lambda data: VolumeFlags(data[0]),
230
236
  )
bumble/profiles/vocs.py CHANGED
@@ -24,17 +24,19 @@ from bumble.device import Connection
24
24
  from bumble.att import ATT_Error
25
25
  from bumble.gatt import (
26
26
  Characteristic,
27
- DelegatedCharacteristicAdapter,
28
27
  TemplateService,
29
28
  CharacteristicValue,
30
- UTF8CharacteristicAdapter,
31
- InvalidServiceError,
32
29
  GATT_VOLUME_OFFSET_CONTROL_SERVICE,
33
30
  GATT_VOLUME_OFFSET_STATE_CHARACTERISTIC,
34
31
  GATT_AUDIO_LOCATION_CHARACTERISTIC,
35
32
  GATT_VOLUME_OFFSET_CONTROL_POINT_CHARACTERISTIC,
36
33
  GATT_AUDIO_OUTPUT_DESCRIPTION_CHARACTERISTIC,
37
34
  )
35
+ from bumble.gatt_adapters import (
36
+ DelegatedCharacteristicProxyAdapter,
37
+ SerializableCharacteristicProxyAdapter,
38
+ UTF8CharacteristicProxyAdapter,
39
+ )
38
40
  from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
39
41
  from bumble.utils import OpenIntEnum
40
42
  from bumble.profiles.bap import AudioLocation
@@ -67,7 +69,7 @@ class ErrorCode(OpenIntEnum):
67
69
  class VolumeOffsetState:
68
70
  volume_offset: int = 0
69
71
  change_counter: int = 0
70
- attribute_value: Optional[CharacteristicValue] = None
72
+ attribute: Optional[Characteristic] = None
71
73
 
72
74
  def __bytes__(self) -> bytes:
73
75
  return struct.pack('<hB', self.volume_offset, self.change_counter)
@@ -81,10 +83,8 @@ class VolumeOffsetState:
81
83
  self.change_counter = (self.change_counter + 1) % (CHANGE_COUNTER_MAX_VALUE + 1)
82
84
 
83
85
  async def notify_subscribers_via_connection(self, connection: Connection) -> None:
84
- assert self.attribute_value is not None
85
- await connection.device.notify_subscribers(
86
- attribute=self.attribute_value, value=bytes(self)
87
- )
86
+ assert self.attribute is not None
87
+ await connection.device.notify_subscribers(attribute=self.attribute)
88
88
 
89
89
  def on_read(self, _connection: Optional[Connection]) -> bytes:
90
90
  return bytes(self)
@@ -93,7 +93,7 @@ class VolumeOffsetState:
93
93
  @dataclass
94
94
  class VocsAudioLocation:
95
95
  audio_location: AudioLocation = AudioLocation.NOT_ALLOWED
96
- attribute_value: Optional[CharacteristicValue] = None
96
+ attribute: Optional[Characteristic] = None
97
97
 
98
98
  def __bytes__(self) -> bytes:
99
99
  return struct.pack('<I', self.audio_location)
@@ -108,12 +108,10 @@ class VocsAudioLocation:
108
108
 
109
109
  async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
110
110
  assert connection
111
- assert self.attribute_value
111
+ assert self.attribute
112
112
 
113
113
  self.audio_location = AudioLocation(int.from_bytes(value, 'little'))
114
- await connection.device.notify_subscribers(
115
- attribute=self.attribute_value, value=value
116
- )
114
+ await connection.device.notify_subscribers(attribute=self.attribute)
117
115
 
118
116
 
119
117
  @dataclass
@@ -152,7 +150,7 @@ class VolumeOffsetControlPoint:
152
150
  @dataclass
153
151
  class AudioOutputDescription:
154
152
  audio_output_description: str = ''
155
- attribute_value: Optional[CharacteristicValue] = None
153
+ attribute: Optional[Characteristic] = None
156
154
 
157
155
  @classmethod
158
156
  def from_bytes(cls, data: bytes):
@@ -166,12 +164,10 @@ class AudioOutputDescription:
166
164
 
167
165
  async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
168
166
  assert connection
169
- assert self.attribute_value
167
+ assert self.attribute
170
168
 
171
169
  self.audio_output_description = value.decode('utf-8')
172
- await connection.device.notify_subscribers(
173
- attribute=self.attribute_value, value=value
174
- )
170
+ await connection.device.notify_subscribers(attribute=self.attribute)
175
171
 
176
172
 
177
173
  # -----------------------------------------------------------------------------
@@ -203,48 +199,45 @@ class VolumeOffsetControlService(TemplateService):
203
199
  VolumeOffsetControlPoint(self.volume_offset_state)
204
200
  )
205
201
 
206
- self.volume_offset_state_characteristic = DelegatedCharacteristicAdapter(
207
- Characteristic(
208
- uuid=GATT_VOLUME_OFFSET_STATE_CHARACTERISTIC,
209
- properties=(
210
- Characteristic.Properties.READ | Characteristic.Properties.NOTIFY
211
- ),
212
- permissions=Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
213
- value=CharacteristicValue(read=self.volume_offset_state.on_read),
202
+ self.volume_offset_state_characteristic: Characteristic[bytes] = Characteristic(
203
+ uuid=GATT_VOLUME_OFFSET_STATE_CHARACTERISTIC,
204
+ properties=(
205
+ Characteristic.Properties.READ | Characteristic.Properties.NOTIFY
214
206
  ),
215
- encode=lambda value: bytes(value),
207
+ permissions=Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
208
+ value=CharacteristicValue(read=self.volume_offset_state.on_read),
216
209
  )
217
210
 
218
- self.audio_location_characteristic = DelegatedCharacteristicAdapter(
219
- Characteristic(
220
- uuid=GATT_AUDIO_LOCATION_CHARACTERISTIC,
221
- properties=(
222
- Characteristic.Properties.READ
223
- | Characteristic.Properties.NOTIFY
224
- | Characteristic.Properties.WRITE_WITHOUT_RESPONSE
225
- ),
226
- permissions=(
227
- Characteristic.Permissions.READ_REQUIRES_ENCRYPTION
228
- | Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION
229
- ),
230
- value=CharacteristicValue(
231
- read=self.audio_location.on_read,
232
- write=self.audio_location.on_write,
233
- ),
211
+ self.audio_location_characteristic: Characteristic[bytes] = Characteristic(
212
+ uuid=GATT_AUDIO_LOCATION_CHARACTERISTIC,
213
+ properties=(
214
+ Characteristic.Properties.READ
215
+ | Characteristic.Properties.NOTIFY
216
+ | Characteristic.Properties.WRITE_WITHOUT_RESPONSE
217
+ ),
218
+ permissions=(
219
+ Characteristic.Permissions.READ_REQUIRES_ENCRYPTION
220
+ | Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION
221
+ ),
222
+ value=CharacteristicValue(
223
+ read=self.audio_location.on_read,
224
+ write=self.audio_location.on_write,
234
225
  ),
235
- encode=lambda value: bytes(value),
236
- decode=VocsAudioLocation.from_bytes,
237
226
  )
238
- self.audio_location.attribute_value = self.audio_location_characteristic.value
227
+ self.audio_location.attribute = self.audio_location_characteristic
239
228
 
240
- self.volume_offset_control_point_characteristic = Characteristic(
241
- uuid=GATT_VOLUME_OFFSET_CONTROL_POINT_CHARACTERISTIC,
242
- properties=Characteristic.Properties.WRITE,
243
- permissions=Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION,
244
- value=CharacteristicValue(write=self.volume_offset_control_point.on_write),
229
+ self.volume_offset_control_point_characteristic: Characteristic[bytes] = (
230
+ Characteristic(
231
+ uuid=GATT_VOLUME_OFFSET_CONTROL_POINT_CHARACTERISTIC,
232
+ properties=Characteristic.Properties.WRITE,
233
+ permissions=Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION,
234
+ value=CharacteristicValue(
235
+ write=self.volume_offset_control_point.on_write
236
+ ),
237
+ )
245
238
  )
246
239
 
247
- self.audio_output_description_characteristic = DelegatedCharacteristicAdapter(
240
+ self.audio_output_description_characteristic: Characteristic[bytes] = (
248
241
  Characteristic(
249
242
  uuid=GATT_AUDIO_OUTPUT_DESCRIPTION_CHARACTERISTIC,
250
243
  properties=(
@@ -262,9 +255,8 @@ class VolumeOffsetControlService(TemplateService):
262
255
  ),
263
256
  )
264
257
  )
265
-
266
- self.audio_output_description.attribute_value = (
267
- self.audio_output_description_characteristic.value
258
+ self.audio_output_description.attribute = (
259
+ self.audio_output_description_characteristic
268
260
  )
269
261
 
270
262
  super().__init__(
@@ -287,44 +279,29 @@ class VolumeOffsetControlServiceProxy(ProfileServiceProxy):
287
279
  def __init__(self, service_proxy: ServiceProxy) -> None:
288
280
  self.service_proxy = service_proxy
289
281
 
290
- if not (
291
- characteristics := service_proxy.get_characteristics_by_uuid(
282
+ self.volume_offset_state = SerializableCharacteristicProxyAdapter(
283
+ service_proxy.get_required_characteristic_by_uuid(
292
284
  GATT_VOLUME_OFFSET_STATE_CHARACTERISTIC
293
- )
294
- ):
295
- raise InvalidServiceError("Volume Offset State characteristic not found")
296
- self.volume_offset_state = DelegatedCharacteristicAdapter(
297
- characteristics[0], decode=VolumeOffsetState.from_bytes
285
+ ),
286
+ VolumeOffsetState,
298
287
  )
299
288
 
300
- if not (
301
- characteristics := service_proxy.get_characteristics_by_uuid(
289
+ self.audio_location = DelegatedCharacteristicProxyAdapter(
290
+ service_proxy.get_required_characteristic_by_uuid(
302
291
  GATT_AUDIO_LOCATION_CHARACTERISTIC
303
- )
304
- ):
305
- raise InvalidServiceError("Audio Location characteristic not found")
306
- self.audio_location = DelegatedCharacteristicAdapter(
307
- characteristics[0],
308
- encode=lambda value: bytes(value),
309
- decode=VocsAudioLocation.from_bytes,
292
+ ),
293
+ encode=lambda value: bytes([int(value)]),
294
+ decode=lambda data: AudioLocation(data[0]),
310
295
  )
311
296
 
312
- if not (
313
- characteristics := service_proxy.get_characteristics_by_uuid(
297
+ self.volume_offset_control_point = (
298
+ service_proxy.get_required_characteristic_by_uuid(
314
299
  GATT_VOLUME_OFFSET_CONTROL_POINT_CHARACTERISTIC
315
300
  )
316
- ):
317
- raise InvalidServiceError(
318
- "Volume Offset Control Point characteristic not found"
319
- )
320
- self.volume_offset_control_point = characteristics[0]
301
+ )
321
302
 
322
- if not (
323
- characteristics := service_proxy.get_characteristics_by_uuid(
303
+ self.audio_output_description = UTF8CharacteristicProxyAdapter(
304
+ service_proxy.get_required_characteristic_by_uuid(
324
305
  GATT_AUDIO_OUTPUT_DESCRIPTION_CHARACTERISTIC
325
306
  )
326
- ):
327
- raise InvalidServiceError(
328
- "Audio Output Description characteristic not found"
329
- )
330
- self.audio_output_description = UTF8CharacteristicAdapter(characteristics[0])
307
+ )