From 479be9a32fea49a1dc26b89f4d2684772b956513 Mon Sep 17 00:00:00 2001 From: isra el Date: Tue, 10 Nov 2026 17:19:28 +0300 Subject: [PATCH] feat: add sim_slot_index to send_sms Choose the SIM by its slot (0 is SIM 1). sim_subscription_id is deprecated. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017B6pTTFVhKWwcKLit7MEdz --- README.md | 15 ++++++++++----- src/textbee/_client.py | 20 ++++++++++++++++---- src/textbee/_types.py | 1 + tests/test_client.py | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e3b1460..6c3c2a6 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,8 @@ textbee.send_sms( | Option | Type | What it does | | --- | --- | --- | | `device_id` | `str` | Which phone sends the message. Omit it and textbee uses your default device, or the enabled device with the most recent heartbeat. | -| `sim_subscription_id` | `int` | Which SIM sends the message on a multi-SIM phone. Omit it and the phone uses its configured preferred SIM, or the system default. | +| `sim_slot_index` | `int` | Which SIM sends the message, by its slot in the phone. `0` is SIM 1, `1` is SIM 2. Omit it and the phone uses its default SIM. | +| `sim_subscription_id` | `int` | Deprecated. Use `sim_slot_index`. See [Choosing a SIM](#choosing-a-sim). | | `scheduled_at` | `str \| datetime` | Send later instead of now. An ISO 8601 string or a timezone-aware `datetime`, in the future. | ```python @@ -55,7 +56,7 @@ textbee.send_sms( recipients=["+12025550123"], message="Your appointment is tomorrow at 9am", device_id="65f0000000000000000000aa", - sim_subscription_id=2, + sim_slot_index=1, scheduled_at=datetime.now(timezone.utc) + timedelta(hours=1), ) ``` @@ -74,11 +75,15 @@ if "warning" in result: print(result["warning"]["message"]) ``` -### Finding your sim_subscription_id +### Choosing a SIM -Open the textbee Android app, go to **Dashboard**, and find the **SIM Cards** section. Each SIM shows its subscription id with a copy button. +Use `sim_slot_index=0` for SIM 1 and `sim_slot_index=1` for SIM 2. The slot is the place of the SIM in the phone. It stays the same when you take the SIM out and put it back. -Be aware that this value is not validated. If the id does not match a SIM currently in the phone it is ignored, and the message goes out from the preferred or default SIM instead. Nothing errors, so confirm which SIM was used by checking the number the message arrived from. +The slot is the place in the phone, not a specific card. A different card in that slot sends from the new card. + +It works with or without `device_id`. If the slot has no active SIM, the message fails with the error code `SIM_SLOT_EMPTY`. textbee never sends it from another SIM. With textbee app versions before slot support, the server refuses a slot it cannot map from the phone's last SIM report with HTTP 400, so the call raises an error. + +`sim_subscription_id` still works, but it is deprecated. It is an Android id that changes when a SIM is reinserted. A stale id sends from the default SIM and nothing errors. If you pass both options, textbee uses `sim_slot_index` and ignores the id. ## Devices diff --git a/src/textbee/_client.py b/src/textbee/_client.py index fbbbc0b..b66a5e1 100644 --- a/src/textbee/_client.py +++ b/src/textbee/_client.py @@ -84,6 +84,7 @@ def send_sms( message: str, recipients: Sequence[str], device_id: str | None = None, + sim_slot_index: int | None = None, sim_subscription_id: int | None = None, scheduled_at: DateInput | None = None, ) -> SendSmsResponse: @@ -101,10 +102,19 @@ def send_sms( id fails with 400 rather than falling back, so a typo cannot send from the wrong phone. - ``sim_subscription_id`` picks the SIM on a multi-SIM phone. It is not - validated: an id that does not match a SIM in the phone is ignored and - the message goes out from the SIM configured in the app, or the system - default. Find the id in the Android app under Dashboard, "SIM Cards". + ``sim_slot_index`` picks the SIM by its slot in the phone: ``0`` is + SIM 1 and ``1`` is SIM 2. It works with or without ``device_id``. If the + slot has no active SIM, the message fails with the error code + ``SIM_SLOT_EMPTY`` and is never sent from another SIM. The slot is the + place in the phone, not a specific card: a different card in that slot + sends from the new card. With textbee app versions before slot support, + the server refuses a slot it cannot map from the phone's last SIM report + with HTTP 400, so the call raises. + + ``sim_subscription_id`` is deprecated: use ``sim_slot_index``. It is an + Android id that changes when a SIM is reinserted, and a stale id sends + from the default SIM with no error. It still works. If you pass both, + textbee uses ``sim_slot_index`` and ignores the id. ``scheduled_at`` sends later instead of now, as an ISO 8601 string or a timezone-aware ``datetime`` in the future. @@ -118,6 +128,8 @@ def send_sms( } if device_id is not None: body["deviceId"] = device_id + if sim_slot_index is not None: + body["simSlotIndex"] = sim_slot_index if sim_subscription_id is not None: body["simSubscriptionId"] = sim_subscription_id if scheduled_at is not None: diff --git a/src/textbee/_types.py b/src/textbee/_types.py index 23de68b..11828a5 100644 --- a/src/textbee/_types.py +++ b/src/textbee/_types.py @@ -190,6 +190,7 @@ class _MessageFields(_MessageRequired, total=False): Oldest first. At most 15: the first 5 and the newest 10. """ + simSlotIndex: int simSubscriptionId: int smsBatch: str createdAt: str diff --git a/tests/test_client.py b/tests/test_client.py index 2f48edf..7c4b125 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -160,6 +160,38 @@ def test_passes_optional_fields_through_and_omits_them_entirely_when_unset( assert set(http.last.body) == {"message", "recipients"} + def test_sim_slot_index_is_sent_including_zero(self, http: FakeUrlopen) -> None: + http.respond(200, {"data": {"successCount": 1}}) + + client().send_sms(message="a", recipients=["+15555550123"], sim_slot_index=0) + + assert http.last.body == { + "message": "a", + "recipients": ["+15555550123"], + "simSlotIndex": 0, + } + + def test_both_sim_options_are_sent(self, http: FakeUrlopen) -> None: + http.respond(200, {"data": {"successCount": 1}}) + + client().send_sms( + message="both", + recipients=["+15555550123"], + sim_slot_index=1, + sim_subscription_id=2, + ) + + assert http.last.body["simSlotIndex"] == 1 + assert http.last.body["simSubscriptionId"] == 2 + + def test_no_sim_option_sends_no_sim_keys(self, http: FakeUrlopen) -> None: + http.respond(200, {"data": {"successCount": 1}}) + + client().send_sms(message="none", recipients=["+15555550123"]) + + assert "simSlotIndex" not in http.last.body + assert "simSubscriptionId" not in http.last.body + def test_accepts_any_sequence_of_recipients(self, http: FakeUrlopen) -> None: http.respond(200, {"data": {"successCount": 2}})