| # Copyright 2024 Google LLC | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # https://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| # ----------------------------------------------------------------------------- | |
| # Imports | |
| # ----------------------------------------------------------------------------- | |
| import asyncio | |
| import logging | |
| import sys | |
| import pytest | |
| import pytest_asyncio | |
| from bumble import att, device | |
| from bumble.keys import PairingKeys | |
| from bumble.profiles import hap | |
| from .test_utils import TwoDevices | |
| # ----------------------------------------------------------------------------- | |
| # Logging | |
| # ----------------------------------------------------------------------------- | |
| logger = logging.getLogger(__name__) | |
| logger.setLevel(logging.DEBUG) | |
| foo_preset = hap.PresetRecord(1, "foo preset") | |
| bar_preset = hap.PresetRecord(50, "bar preset") | |
| foobar_preset = hap.PresetRecord(5, "foobar preset") | |
| unavailable_preset = hap.PresetRecord( | |
| 78, | |
| "foobar preset", | |
| hap.PresetRecord.Property( | |
| hap.PresetRecord.Property.Writable.CANNOT_BE_WRITTEN, | |
| hap.PresetRecord.Property.IsAvailable.IS_UNAVAILABLE, | |
| ), | |
| ) | |
| server_features = hap.HearingAidFeatures( | |
| hap.HearingAidType.MONAURAL_HEARING_AID, | |
| hap.PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED, | |
| hap.IndependentPresets.IDENTICAL_PRESET_RECORD, | |
| hap.DynamicPresets.PRESET_RECORDS_DOES_NOT_CHANGE, | |
| hap.WritablePresetsSupport.WRITABLE_PRESET_RECORDS_SUPPORTED, | |
| ) | |
| TIMEOUT = 0.1 | |
| async def assert_queue_is_empty(queue: asyncio.Queue): | |
| assert queue.empty() | |
| # Check that nothing is being added during TIMEOUT secondes | |
| if sys.version_info >= (3, 11): | |
| with pytest.raises(TimeoutError): | |
| await asyncio.wait_for(queue.get(), TIMEOUT) | |
| else: | |
| with pytest.raises(asyncio.TimeoutError): | |
| await asyncio.wait_for(queue.get(), TIMEOUT) | |
| # ----------------------------------------------------------------------------- | |
| @pytest_asyncio.fixture | |
| async def hap_client(): | |
| devices = TwoDevices() | |
| devices[0].add_service( | |
| hap.HearingAccessService( | |
| devices[0], | |
| server_features, | |
| [foo_preset, bar_preset, foobar_preset, unavailable_preset], | |
| ) | |
| ) | |
| await devices.setup_connection() | |
| # Mock encryption. | |
| devices.connections[0].encryption = 1 # type: ignore | |
| devices.connections[1].encryption = 1 # type: ignore | |
| devices[0].on_pairing( | |
| devices.connections[0], devices.connections[0].peer_address, PairingKeys(), True | |
| ) | |
| peer = device.Peer(devices.connections[1]) # type: ignore | |
| await peer.request_mtu(49) | |
| peer2 = device.Peer(devices.connections[0]) # type: ignore | |
| await peer2.request_mtu(49) | |
| hap_client = await peer.discover_service_and_create_proxy( | |
| hap.HearingAccessServiceProxy | |
| ) | |
| assert hap_client | |
| await hap_client.setup_subscription() | |
| yield hap_client | |
| # ----------------------------------------------------------------------------- | |
| @pytest.mark.asyncio | |
| async def test_init_service(hap_client: hap.HearingAccessServiceProxy): | |
| assert ( | |
| hap.HearingAidFeatures_from_bytes(await hap_client.server_features.read_value()) | |
| == server_features | |
| ) | |
| assert (await hap_client.active_preset_index.read_value()) == (foo_preset.index) | |
| # ----------------------------------------------------------------------------- | |
| @pytest.mark.asyncio | |
| async def test_read_all_presets(hap_client: hap.HearingAccessServiceProxy): | |
| await hap_client.hearing_aid_preset_control_point.write_value( | |
| bytes([hap.HearingAidPresetControlPointOpcode.READ_PRESETS_REQUEST, 1, 0xFF]) | |
| ) | |
| assert (await hap_client.preset_control_point_indications.get()) == bytes( | |
| [hap.HearingAidPresetControlPointOpcode.READ_PRESET_RESPONSE, 0] | |
| ) + bytes(foo_preset) | |
| assert (await hap_client.preset_control_point_indications.get()) == bytes( | |
| [hap.HearingAidPresetControlPointOpcode.READ_PRESET_RESPONSE, 0] | |
| ) + bytes(foobar_preset) | |
| assert (await hap_client.preset_control_point_indications.get()) == bytes( | |
| [hap.HearingAidPresetControlPointOpcode.READ_PRESET_RESPONSE, 0] | |
| ) + bytes(bar_preset) | |
| assert (await hap_client.preset_control_point_indications.get()) == bytes( | |
| [hap.HearingAidPresetControlPointOpcode.READ_PRESET_RESPONSE, 1] | |
| ) + bytes(unavailable_preset) | |
| await assert_queue_is_empty(hap_client.preset_control_point_indications) | |
| # ----------------------------------------------------------------------------- | |
| @pytest.mark.asyncio | |
| async def test_read_partial_presets(hap_client: hap.HearingAccessServiceProxy): | |
| await hap_client.hearing_aid_preset_control_point.write_value( | |
| bytes([hap.HearingAidPresetControlPointOpcode.READ_PRESETS_REQUEST, 3, 2]) | |
| ) | |
| assert (await hap_client.preset_control_point_indications.get())[2:] == bytes( | |
| foobar_preset | |
| ) | |
| assert (await hap_client.preset_control_point_indications.get())[2:] == bytes( | |
| bar_preset | |
| ) | |
| # ----------------------------------------------------------------------------- | |
| @pytest.mark.asyncio | |
| async def test_set_active_preset_valid(hap_client: hap.HearingAccessServiceProxy): | |
| await hap_client.hearing_aid_preset_control_point.write_value( | |
| bytes( | |
| [hap.HearingAidPresetControlPointOpcode.SET_ACTIVE_PRESET, bar_preset.index] | |
| ) | |
| ) | |
| assert (await hap_client.active_preset_index_notification.get()) == bar_preset.index | |
| assert (await hap_client.active_preset_index.read_value()) == (bar_preset.index) | |
| await assert_queue_is_empty(hap_client.active_preset_index_notification) | |
| # ----------------------------------------------------------------------------- | |
| @pytest.mark.asyncio | |
| async def test_set_active_preset_invalid(hap_client: hap.HearingAccessServiceProxy): | |
| with pytest.raises(att.ATT_Error) as e: | |
| await hap_client.hearing_aid_preset_control_point.write_value( | |
| bytes( | |
| [ | |
| hap.HearingAidPresetControlPointOpcode.SET_ACTIVE_PRESET, | |
| unavailable_preset.index, | |
| ] | |
| ), | |
| with_response=True, | |
| ) | |
| assert e.value.error_code == hap.ErrorCode.PRESET_OPERATION_NOT_POSSIBLE | |
| # ----------------------------------------------------------------------------- | |
| @pytest.mark.asyncio | |
| async def test_set_next_preset(hap_client: hap.HearingAccessServiceProxy): | |
| await hap_client.hearing_aid_preset_control_point.write_value( | |
| bytes([hap.HearingAidPresetControlPointOpcode.SET_NEXT_PRESET]) | |
| ) | |
| assert ( | |
| await hap_client.active_preset_index_notification.get() | |
| ) == foobar_preset.index | |
| assert (await hap_client.active_preset_index.read_value()) == (foobar_preset.index) | |
| await assert_queue_is_empty(hap_client.active_preset_index_notification) | |
| # ----------------------------------------------------------------------------- | |
| @pytest.mark.asyncio | |
| async def test_set_next_preset_will_loop_to_first( | |
| hap_client: hap.HearingAccessServiceProxy, | |
| ): | |
| async def go_next(new_preset: hap.PresetRecord): | |
| await hap_client.hearing_aid_preset_control_point.write_value( | |
| bytes([hap.HearingAidPresetControlPointOpcode.SET_NEXT_PRESET]) | |
| ) | |
| assert ( | |
| await hap_client.active_preset_index_notification.get() | |
| ) == new_preset.index | |
| assert (await hap_client.active_preset_index.read_value()) == (new_preset.index) | |
| await go_next(foobar_preset) | |
| await go_next(bar_preset) | |
| await go_next(foo_preset) | |
| # Note that there is a invalid preset in the preset record of the server | |
| await assert_queue_is_empty(hap_client.active_preset_index_notification) | |
| # ----------------------------------------------------------------------------- | |
| @pytest.mark.asyncio | |
| async def test_set_previous_preset_will_loop_to_last( | |
| hap_client: hap.HearingAccessServiceProxy, | |
| ): | |
| await hap_client.hearing_aid_preset_control_point.write_value( | |
| bytes([hap.HearingAidPresetControlPointOpcode.SET_PREVIOUS_PRESET]) | |
| ) | |
| assert (await hap_client.active_preset_index_notification.get()) == bar_preset.index | |
| assert (await hap_client.active_preset_index.read_value()) == (bar_preset.index) | |
| await assert_queue_is_empty(hap_client.active_preset_index_notification) |