diff --git a/src/uiprotect/api.py b/src/uiprotect/api.py index 07f8156a..a63fb191 100644 --- a/src/uiprotect/api.py +++ b/src/uiprotect/api.py @@ -1806,10 +1806,12 @@ async def play_speaker( *, volume: int | None = None, repeat_times: int | None = None, + ringtone_id: str | None = None, + track_no: int | None = None, ) -> None: """Plays chime tones on a chime""" data: dict[str, Any] | None = None - if volume or repeat_times: + if volume or repeat_times or ringtone_id or track_no: chime = self.bootstrap.chimes.get(device_id) if chime is None: raise BadRequest("Invalid chime ID %s", device_id) @@ -1817,8 +1819,11 @@ async def play_speaker( data = { "volume": volume or chime.volume, "repeatTimes": repeat_times or chime.repeat_times, - "trackNo": chime.track_no, + "trackNo": track_no or chime.track_no, } + if ringtone_id: + data["ringtoneId"] = ringtone_id + data.pop("trackNo", None) await self.api_request( f"chimes/{device_id}/play-speaker", diff --git a/src/uiprotect/data/bootstrap.py b/src/uiprotect/data/bootstrap.py index ea5c3e16..4e59a77b 100644 --- a/src/uiprotect/data/bootstrap.py +++ b/src/uiprotect/data/bootstrap.py @@ -30,6 +30,7 @@ Doorlock, Light, ProtectAdoptableDeviceModel, + Ringtone, Sensor, Viewer, ) @@ -183,6 +184,7 @@ class Bootstrap(ProtectBaseObject): doorlocks: dict[str, Doorlock] chimes: dict[str, Chime] aiports: dict[str, AiPort] + ringtones: list[Ringtone] last_update_id: str # TODO: diff --git a/src/uiprotect/data/devices.py b/src/uiprotect/data/devices.py index 31f98c76..2eab9595 100644 --- a/src/uiprotect/data/devices.py +++ b/src/uiprotect/data/devices.py @@ -3388,3 +3388,12 @@ def callback() -> None: class AiPort(Camera): paired_cameras: list[str] + + +class Ringtone(ProtectBaseObject): + id: str + name: str + size: int + is_default: bool + nvr_mac: str + model_key: str diff --git a/src/uiprotect/data/types.py b/src/uiprotect/data/types.py index d24be375..0e8a805c 100644 --- a/src/uiprotect/data/types.py +++ b/src/uiprotect/data/types.py @@ -128,6 +128,7 @@ class ModelType(str, UnknownValuesEnumMixin, enum.Enum): DEVICE_GROUP = "deviceGroup" RECORDING_SCHEDULE = "recordingSchedule" ULP_USER = "ulpUser" + RINGTONE = "ringtone" KEYRING = "keyring" UNKNOWN = "unknown" diff --git a/tests/sample_data/sample_bootstrap.json b/tests/sample_data/sample_bootstrap.json index 5bc7a687..6b391ac0 100644 --- a/tests/sample_data/sample_bootstrap.json +++ b/tests/sample_data/sample_bootstrap.json @@ -7276,6 +7276,24 @@ "modelKey": "chime" } ], + "ringtones": [ + { + "id": "66a14fa502d44203e40003eb", + "name": "Default", + "size": 208, + "isDefault": true, + "nvrMac": "4B8290F6D7A3", + "modelKey": "ringtone" + }, + { + "id": "66a14fa502da4203e40003ec", + "name": "custometest", + "size": 180, + "isDefault": false, + "nvrMac": "4B8290F6D7A3", + "modelKey": "ringtone" + } + ], "aiports": [ { "isDeleting": false, diff --git a/tests/test_api.py b/tests/test_api.py index 4c867fbe..f872f871 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -969,3 +969,99 @@ async def test_get_aiports(protect_client: ProtectApiClient, aiports): objs = [create_from_unifi_dict(d) for d in aiports] assert_equal_dump(objs, await protect_client.get_aiports()) + + +@pytest.mark.asyncio() +async def test_play_speaker(protect_client: ProtectApiClient): + """Test play_speaker with default parameters.""" + device_id = "cf1a330397c08f919d02bd7c" + protect_client.api_request = AsyncMock() + + await protect_client.play_speaker(device_id) + + protect_client.api_request.assert_called_with( + f"chimes/{device_id}/play-speaker", + method="post", + json=None, + ) + + +@pytest.mark.asyncio() +async def test_play_speaker_with_volume(protect_client: ProtectApiClient): + """Test play_speaker with volume parameter.""" + device_id = "cf1a330397c08f919d02bd7c" + volume = 5 + chime = protect_client.bootstrap.chimes[device_id] + protect_client.api_request = AsyncMock() + + await protect_client.play_speaker(device_id, volume=volume) + + protect_client.api_request.assert_called_with( + f"chimes/{device_id}/play-speaker", + method="post", + json={ + "volume": volume, + "repeatTimes": chime.repeat_times, + "trackNo": chime.track_no, + }, + ) + + +@pytest.mark.asyncio() +async def test_play_speaker_with_ringtone_id(protect_client: ProtectApiClient): + """Test play_speaker with ringtone_id parameter.""" + device_id = "cf1a330397c08f919d02bd7c" + ringtone_id = "ringtone_1" + chime = protect_client.bootstrap.chimes[device_id] + protect_client.api_request = AsyncMock() + + await protect_client.play_speaker(device_id, ringtone_id=ringtone_id) + + protect_client.api_request.assert_called_with( + f"chimes/{device_id}/play-speaker", + method="post", + json={ + "volume": chime.volume, + "repeatTimes": chime.repeat_times, + "ringtoneId": ringtone_id, + }, + ) + + +@pytest.mark.asyncio() +async def test_play_speaker_invalid_chime_id(protect_client: ProtectApiClient): + """Test play_speaker with invalid chime ID.""" + device_id = "invalid_id" + protect_client.api_request = AsyncMock() + + with pytest.raises(BadRequest): + await protect_client.play_speaker(device_id, volume=5) + + +@pytest.mark.asyncio() +async def test_play_speaker_with_all_parameters(protect_client: ProtectApiClient): + """Test play_speaker with all parameters.""" + device_id = "cf1a330397c08f919d02bd7c" + volume = 5 + repeat_times = 3 + ringtone_id = "ringtone_1" + track_no = 2 + protect_client.api_request = AsyncMock() + + await protect_client.play_speaker( + device_id, + volume=volume, + repeat_times=repeat_times, + ringtone_id=ringtone_id, + track_no=track_no, + ) + + protect_client.api_request.assert_called_with( + f"chimes/{device_id}/play-speaker", + method="post", + json={ + "volume": volume, + "repeatTimes": repeat_times, + "ringtoneId": ringtone_id, + }, + )