Standard API LAN (Game Mode) client. Sends commands via HTTP/HTTPS to the Lovense app on the same network.
For async applications (Discord, Telegram, FastAPI, workers), use AsyncLANClient.
LANClient(
app_name: str,
local_ip: str | None = None,
*,
domain: str | None = None,
port: int = 20011,
ssl_port: int = 30011,
use_https: bool = False,
verify_ssl: bool = True,
timeout: float = 10.0,
)| Parameter | Type | Default | Description |
|---|---|---|---|
app_name |
str | — | Application name (e.g. "MyApp") |
local_ip |
str | None | Device IP (e.g. "192.168.1.100"). Use with domain=None. |
domain |
str | None | Pre-built domain (e.g. "192-168-1-100.lovense.club"). Use when you have domain from Socket API. |
port |
int | 20011 | HTTP port (Lovense Remote: 20011, Connect: 34567) |
ssl_port |
int | 30011 | HTTPS port |
use_https |
bool | False | Use HTTPS instead of HTTP |
verify_ssl |
bool | True | Verify SSL cert. If False, uses fingerprint pinning. |
timeout |
float | 10.0 | Request timeout in seconds |
Example:
client = LANClient("MyApp", "192.168.1.100", port=20011)Class method: LANClient.from_device_info(app_name, domain, https_port=30011, **kwargs) — Create from Socket API device info (e.g. basicapi_update_device_info_tc payload).
| Method | Parameters | Returns | Description |
|---|---|---|---|
get_toys() |
— | GetToysResponse |
Get connected toys. Uses a typed data.toys[] list. |
get_toys_name() |
— | GetToyNameResponse |
Get connected toy names. |
function_request(actions, time=0, loop_on_time=None, loop_off_time=None, toy_id=None, stop_previous=None) |
actions: dict like {Actions.VIBRATE: 10} or per-motor {Actions.VIBRATE1: 12, Actions.VIBRATE2: 6}; toy_id: one id, list, or None for all |
CommandResponse |
Send Function command. time in seconds. |
stop(toy_id=None) |
toy_id: str or list |
CommandResponse |
Stop all motors. |
preset_request(name, time=0, toy_id=None) |
name: Presets enum or str |
CommandResponse |
Send Preset (pulse, wave, etc.). |
pattern_request(pattern, actions=None, interval=100, time=0, toy_id=None) |
pattern: list of 0–20; actions: e.g. [Actions.VIBRATE1] for one motor; toy_id optional |
CommandResponse |
Custom pattern. |
pattern_request_raw(strength, rule="V:1;F:;S:100#", time=0, toy_id=None) |
Raw rule/strength strings | CommandResponse |
Advanced pattern. |
position_request(value, toy_id=None) |
value: 0–100 |
CommandResponse |
Position for Solace Pro. |
pattern_v2_setup(actions) |
actions: list of {ts, pos} |
CommandResponse |
PatternV2 Setup. |
pattern_v2_play(toy_id=None, start_time=None, offset_time=None, time_ms=None) |
— | CommandResponse |
PatternV2 Play. |
pattern_v2_init_play(actions, toy_id=None, ...) |
— | CommandResponse |
PatternV2 Setup + Play. |
pattern_v2_stop(toy_id=None) |
— | CommandResponse |
PatternV2 Stop. |
pattern_v2_sync_time() |
— | CommandResponse |
PatternV2 SyncTime. |
send_command(command_data, timeout=None) |
Raw command dict | dict |
Low-level; returns raw dict. Raises LovenseError on failures. |
decode_response(response) |
Response dict | str | Human-readable response string. |
Examples:
import time
with client.play({Actions.VIBRATE: 10}, time=5, toy_id="T123"):
time.sleep(5)
# One toy, separate motors (Edge-class); check channels with features_for_toy(toy_dict)
client.function_request({Actions.VIBRATE1: 14}, time=2, toy_id="T123")
client.function_request({Actions.VIBRATE2: 8}, time=2, toy_id="T123")
# Pattern on motor 2 only
client.pattern_request([6, 12, 18], time=4, toy_id="T123", actions=[Actions.VIBRATE2])See also the LAN tutorial.
Standard API Server client. Sends commands via Lovense cloud. Requires developer token and uid from QR pairing.
ServerClient(
developer_token: str,
uid: str,
timeout: float = 10.0,
)| Parameter | Type | Description |
|---|---|---|
developer_token |
str | From Lovense Developer Dashboard |
uid |
str | User ID from QR pairing callback |
timeout |
float | Request timeout |
Aligned with :class:LANClient for swapping transports: get_toys, get_toys_name, function_request, stop, play, preset_request, pattern_request, pattern_request_raw, send_command, decode_response.
pattern_request accepts either a list of strengths (same as LAN) or raw (rule, strength) positional strings; pattern_request_raw(strength, rule=..., ...) matches LAN parameter order.
Per toy / per motor: same toy_id and Actions.VIBRATE1 / VIBRATE2 as LANClient (after get_toys() to learn ids).
r = client.get_toys()
toys = {t.id: t.model_dump() for t in r.data.toys} if r.data else {}
for tid in toys:
client.function_request({Actions.VIBRATE: 7}, time=2, toy_id=tid)Abstract base class (abc.ABC) for the shared async control API: same method names and compatible signatures on AsyncLANClient, AsyncServerClient, BleDirectClient, and BleDirectHub. Use it when you want one async def (or class field) that can hold any of those implementations — swap transport by changing only how you construct the client.
Sync LANClient / ServerClient are not subclasses; they mirror the same ideas with blocking calls. For new asyncio apps, prefer the async types + this ABC.
from lovensepy import Actions, LovenseAsyncControlClient, AsyncLANClient, AsyncServerClient
async def pulse_once(client: LovenseAsyncControlClient) -> None:
await client.function_request({Actions.VIBRATE: 8}, time=1.5)
# Pick one transport:
async def via_lan():
async with AsyncLANClient("App", "192.168.1.50", port=20011) as c:
await pulse_once(c)
async def via_server():
async with AsyncServerClient("DEV_TOKEN", "user_uid") as c:
await pulse_once(c)The FastAPI service types its backend as LovenseControlBackend (a Protocol subset of this surface: get_toys, function_request, stop, pattern_request, preset_request). See lovensepy.services.fastapi.
Async version of the Standard API Server client for server-side bots.
Subclasses LovenseAsyncControlClient.
AsyncServerClient is an async client and should be closed when you stop the process:
from lovensepy import AsyncServerClient, Actions
async def run_once():
async with AsyncServerClient("YOUR_DEV_TOKEN", "USER_UID") as client:
await client.function_request({Actions.VIBRATE: 10}, time=2)If you don't use async with, call await client.aclose() explicitly.
High-level async methods accept timeout to override the client default for that single call.
Async version of LAN client for local applications (runs on the same network as the Lovense device).
If you're building a production bot that runs on your server, prefer AsyncServerClient or SocketAPIClient instead.
Subclasses LovenseAsyncControlClient.
AsyncLANClient reuses HTTP sessions for better throughput. Close it when done:
from lovensepy import AsyncLANClient, Actions
async def run_once():
async with AsyncLANClient("MyBot", "192.168.1.100", port=20011) as client:
await client.function_request({Actions.VIBRATE: 10}, time=2)If you do not use async with, call await client.aclose() explicitly.
All high-level async methods accept timeout to override client default timeout for that call:
toys = await client.get_toys(timeout=2.0) # quick call
await client.pattern_request([5, 10, 15, 20], time=20, timeout=15.0) # longer callFor HTTPS with verify_ssl=False, certificate fingerprint verification is guarded internally to avoid duplicate concurrent checks when many commands hit the same endpoint at once.
When your bot runs on a server, you typically use:
AsyncServerClient(Standard API Server): cloud HTTP requests (token +uid)SocketAPIClient(Socket API): cloud WebSocket + event loop
Key idea: your backend must authenticate the incoming request, then resolve the correct Lovense session from your own stored mapping (e.g. in a database). Never accept a Lovense uid (or socket auth token) directly from the user request.
This avoids:
- data conflicts (shared mutable objects across users)
- accidental session mix-ups (sending commands to someone else’s
uid) - security issues (exposing or trusting client-provided session identifiers)
import asyncio
from lovensepy import AsyncServerClient, Actions
class ServerSessionPool:
"""
Keeps per-user clients in memory.
user_id: your app user ID (Discord/Telegram).
lovense_uid: stored in your DB after QR pairing / OAuth-like flow.
"""
def __init__(self):
self._clients: dict[str, AsyncServerClient] = {}
self._lock = asyncio.Lock()
async def get_or_create(self, user_id: str, *, lovense_uid: str) -> AsyncServerClient:
async with self._lock:
client = self._clients.get(user_id)
if client is None:
client = AsyncServerClient(
developer_token="YOUR_DEV_TOKEN",
uid=lovense_uid,
timeout=5.0,
)
self._clients[user_id] = client
return client
async def close_session(self, user_id: str) -> None:
async with self._lock:
client = self._clients.pop(user_id, None)
if client is not None:
await client.aclose()
async def shutdown(self) -> None:
async with self._lock:
clients = list(self._clients.values())
self._clients.clear()
await asyncio.gather(*(c.aclose() for c in clients), return_exceptions=True)
sessions = ServerSessionPool()
async def handle_vibrate(user_id: str, level: int) -> None:
# 1) Authenticate request on your server (Discord/Telegram auth).
# 2) Look up lovense_uid for this authenticated user from your DB.
lovense_uid = "LOOKED_UP_FROM_YOUR_DB"
# 3) Resolve the correct per-user client.
client = await sessions.get_or_create(user_id, lovense_uid=lovense_uid)
# 4) Use per-request timeout if needed.
await client.function_request({Actions.VIBRATE: level}, time=2, timeout=3.0)Scaling notes (server-side):
- Use one shared event loop and non-blocking handlers (
awaiteverywhere). - Reuse clients per user/session; avoid creating them per command.
- Put an upper bound on concurrency (e.g.
asyncio.Semaphore) if a single user or a spike of users can spam commands. - Add idle cleanup (TTL) so inactive sessions are closed automatically.
- For very large loads, shard bot workers/processes and keep per-process session maps.
- If you use
SocketAPIClient, create and keep one WebSocket client per Lovense user session (perws_url/auth token), and route commands through the correct per-session instance just like above.
Async WebSocket client for Socket API. Commands via WebSocket (or LAN HTTPS when use_local_commands=True).
SocketAPIClient(
ws_url: str,
*,
use_local_commands: bool = False,
app_name: str = "lovensepy",
raise_on_disconnect: bool = False,
on_socket_open: Callable | None = None,
on_socket_close: Callable | None = None,
on_socket_error: Callable[[Exception], ...] | None = None,
on_socket_io_connected: Callable | None = None,
on_event: Callable[[str, Any], ...] | None = None,
)| Parameter | Type | Description |
|---|---|---|
ws_url |
str | WebSocket URL from build_websocket_url |
use_local_commands |
bool | Send commands via LAN HTTPS when device on same network |
app_name |
str | App name for local commands |
raise_on_disconnect |
bool | Raise ConnectionError when sending while disconnected |
on_socket_open, on_socket_close, on_socket_error |
Callable | Connection lifecycle callbacks |
on_socket_io_connected |
Callable | Fired when Socket.IO handshake complete |
on_event |
Callable | Fired for each Socket.IO event (event_name, payload) |
| Method | Description |
|---|---|
connect() |
Async. Connect and start background ping/recv tasks (non-blocking). |
run_forever() |
Async. Connect and block until disconnected. |
start_background(auto_reconnect=False, retry_delay=5.0) |
Start run_forever (or reconnect loop) as a task. |
connect_with_retry(retry_delay=5.0, max_retries=None) |
Reconnect loop for 24/7 bots. |
wait_closed() |
Wait until current connection fully closes. |
disconnect() |
Close connection. |
send_command(command, action, time_sec=0, toy=None, ...) |
Send command (non-blocking). |
send_command_await(command, action, ...) |
Send command and await delivery. Use for stops. |
send_event(event, payload=None) |
Send raw Socket.IO event. |
on(event_name) |
Decorator to register per-event handlers. |
add_event_handler(event_name, handler) |
Register per-event handler programmatically. |
Event routing example:
@client.on("basicapi_update_device_info_tc")
async def on_device_info(payload):
print("Device info:", payload)| Property | Type | Description |
|---|---|---|
is_socket_io_connected |
bool | True when Socket.IO handshake done and ready for commands |
is_using_local_commands |
bool | True when commands go via LAN HTTPS |
Async WebSocket client for Toy Events API. Receives real-time events from toys. Lovense Remote only, port 20011.
ToyEventsClient(
ip: str,
port: int = 20011,
use_https: bool = False,
https_port: int = 30011,
app_name: str = "lovensepy",
*,
on_open: Callable | None = None,
on_close: Callable | None = None,
on_error: Callable[[Exception], ...] | None = None,
on_event: Callable[[str, Any], ...] | None = None,
)| Method/Property | Description |
|---|---|
connect() |
Async. Connect, request access, receive events until disconnected. |
disconnect() |
Close connection. |
is_connected |
True if WebSocket connected. |
is_access_granted |
True when user granted access in Lovense Remote. |
Direct BLE control (optional bleak). See Direct BLE for limitations, multi-toy notes, and conflict with Lovense Remote.
Import: from lovensepy import BleDirectClient (lazy) or from lovensepy.ble_direct import BleDirectClient.
Subclasses LovenseAsyncControlClient.
BleDirectClient(
address: str,
*,
uart_tx_uuid: str | None = None,
uart_rx_uuid: str | None = None,
write_with_response: bool = False,
silence_on_link_loss: bool = True,
link_loss_silence_timeout: float = 12.0,
toy_type: str | None = None,
uart_inter_command_delay_s: float = 0.015,
post_timed_function_silence_cooldown_s: float = 0.22,
dual_single_channel_prime_peer_zero: bool = True,
dual_single_channel_prime_delay_s: float = 0.045,
ble_preset_uart_keyword: str = "Pat",
ble_preset_emulate_with_pattern: bool = False,
gatt_write_max_attempts: int = 1,
gatt_write_retry_base_delay: float = 0.2,
gatt_write_retry_max_delay: float = 2.0,
)| Parameter | Description |
|---|---|
address |
BLE device address (OS-specific string, e.g. UUID on macOS). |
uart_tx_uuid |
If set, that characteristic must exist after connect (write flags optional). If None, tries DEFAULT_UART_TX_UUIDS (5030/455a/5330/5730-style Lovense UART, Nordic NUS TX, legacy fff2), then any ????0002-????-4bd4-bbd5-a6920e4c5653 Lovense-family TX. |
uart_rx_uuid |
Optional RX (notify) UUID. If None, the client tries DEFAULT_UART_RX_UUIDS and the sibling of TX (…0002… → …0003…). If nothing matches, query helpers are unavailable but set_vibration/send_uart_command still work. |
write_with_response |
Passed through to bleak’s write_gatt_char(..., response=...). |
silence_on_link_loss |
If true (default), an unexpected disconnect schedules a short reconnect and writes the full stop list from uart_catalog. |
link_loss_silence_timeout |
Seconds for that reconnect + service discovery (default 12). |
toy_type |
Optional Lovense type string (lush, edge, solace pro, …) for :meth:silence_all_motors defaults and for :func:lovensepy.toy_utils.features_for_toy fallbacks. |
uart_inter_command_delay_s |
Seconds to wait between consecutive UART lines sent from one logical update (e.g. dual-motor Vibrate1 then Vibrate2). Default matches :data:DEFAULT_UART_INTER_COMMAND_DELAY_S (~15 ms); use 0 for back-to-back writes without delay. |
post_timed_function_silence_cooldown_s |
After :meth:function_request with time > 0, the client calls :meth:silence_all_motors then waits this many seconds before returning (default :data:DEFAULT_POST_TIMED_FUNCTION_SILENCE_COOLDOWN_S, ~220 ms). Helps the next command apply on some BLE stacks. Use 0 to disable. |
dual_single_channel_prime_peer_zero |
For toys with both Vibrate1 and Vibrate2, when a logical update maps to a single non-zero motor line and the last non-zero motor (tracked across :meth:silence_all_motors) was the peer, send that peer as …:0; in a separate preceding GATT write (default on). Avoids priming on every step (which could feel “one step behind” on some firmware). Set False to disable. Raw :meth:send_uart_command clears the motor memory. |
dual_single_channel_prime_delay_s |
Seconds to wait after the peer-zero prime write and before the main line (default :data:DEFAULT_DUAL_SINGLE_CHANNEL_PRIME_DELAY_S, ~45 ms). Use 0 to omit the delay (still two writes). |
ble_preset_uart_keyword |
UART keyword for presets: Pat or Preset. |
ble_preset_emulate_with_pattern |
If true, map app preset names to stepped patterns over UART when firmware ignores Pat/Preset lines. |
gatt_write_max_attempts |
How many times to attempt each GATT TX write on transient errors (BleakError, timeouts, some OSError). Default 1 = no retry. |
gatt_write_retry_base_delay |
Initial backoff delay in seconds between retries (exponential, capped by gatt_write_retry_max_delay). |
gatt_write_retry_max_delay |
Maximum delay between GATT write retries. |
| Method / property | Description |
|---|---|
async connect() |
Connect, resolve TX UUID (macOS: serialized across clients), raise LovenseBLEError if bleak is missing or no matching characteristic. |
async disconnect() |
Close BLE link. |
async aclose() |
Alias for disconnect(). |
async set_vibration(level) |
Sends Vibrate:{level}; for level in 0..20; skips duplicate consecutive levels. |
async send_uart_command(str, *, encoding, ensure_semicolon) |
Encode and write a UART command string; clears vibrate dedupe. |
async send_uart_bytes(bytes) |
Raw write to TX; clears vibrate dedupe. |
async silence_all_motors(toy_type=None) |
Stop burst: type-specific if toy_type or constructor toy_type is set, else the full uart_catalog list. |
async query_uart_line(command) |
Subscribe to RX, write command, return first …; line (for custom queries). |
async fetch_battery_percent() |
Battery; → percent 0–100. |
async fetch_device_type_fields() |
DeviceType; → :class:DeviceTypeFields (model letter, firmware, BT hex). |
async fetch_ble_snapshot(adv_name=None) |
Battery + device type + suggested_features from LVS-… name slug. |
is_connected |
True when the underlying client reports connected. |
uart_tx_uuid |
Resolved characteristic UUID after connect(), else None. |
uart_rx_uuid |
Resolved RX notify UUID after connect(), else None if not found. |
actions, presets, error_codes |
Same enums / maps as :class:~lovensepy.standard.async_lan.AsyncLANClient (Actions, Presets, ERROR_CODES) for drop-in style code. |
last_command |
Last JSON-style payload passed to send_command, mirroring the LAN client. |
async function_request(...), async stop(...), play(...) |
Standard API names: drive UART instead of HTTP (see lovensepy/ble_direct/standard_compat.py). Returns :class:~lovensepy._models.CommandResponse with data.transport == "ble". |
async pattern_request / pattern_request_raw |
Emulates LAN pattern timing by stepping strengths over UART (not identical to in-app Pattern). |
async preset_request |
Sends UART Pat:{n}; or Preset:{n}; (integer n). Prefix: constructor ble_preset_uart_keyword. Names map through :data:~lovensepy.PRESET_BLE_PAT_INDEX; digit-only name is a raw slot (0–20). |
async position_request |
Sends Position:{0..100}; over UART. |
async send_command |
Routes the same command JSON keys as LAN (Function, Pattern, Preset, Position, …) to UART. |
decode_response |
Same human-readable formatting helper as the async LAN client. |
async get_toys / get_toys_name, async pattern_v2_* |
Raise LovenseBLEError — those flows need the Lovense app bridge (LAN). |
Helpers (same lovensepy.ble_direct import path): scan_lovense_ble_devices(timeout, *, name_prefix="LVS-"|None) → list of (address, name); build_vibrate_command(level) → Vibrate:n; string; DEFAULT_UART_INTER_COMMAND_DELAY_S, DEFAULT_POST_TIMED_FUNCTION_SILENCE_COOLDOWN_S, DEFAULT_DUAL_SINGLE_CHANNEL_PRIME_DELAY_S; ble_uart_features_for_toy_type, ble_stop_command_strings_for_toy_type, DEFAULT_FULL_STOP_COMMANDS, default_full_stop_payloads (UART strings and per-type motor hints); parse_battery_percent, parse_device_type_fields, DeviceTypeFields.
Import: from lovensepy import BleDirectHub (lazy) or from lovensepy.ble_direct import BleDirectHub.
Subclasses LovenseAsyncControlClient.
One object in your code; each registered toy still has its own BleDirectClient and BLE connection. You choose string ids (like LAN toyId), map them to BLE addresses, then call the same method names as AsyncLANClient / BleDirectClient: function_request, preset_request, stop, send_command, get_toys, get_toys_name, play(toy_id=...) (context manager requires one id), etc. toy_id=None (or omitting toy in send_command) means all registered toys.
hub = BleDirectHub()
hub.add_toy("a", "BLE-ADDRESS-1", name="LVS-Edge", toy_type="edge")
hub.add_toy("b", "BLE-ADDRESS-2", name="LVS-Lush")
await hub.connect_all()
await hub.preset_request(hub.presets.PULSE, time=10.0, toy_id=None) # everyone
await hub.function_request({hub.actions.VIBRATE: 5}, toy_id="b") # one
await hub.function_request({hub.actions.VIBRATE1: 10}, time=2, toy_id="a") # Edge-style motor 1
await hub.function_request({hub.actions.VIBRATE2: 8}, time=2, toy_id="a") # motor 2
await hub.disconnect_all()Or skip manual addresses: await hub.discover_and_connect(timeout=10.0) scans LVS-… advertisers, registers stable ids, connects, and optionally reads UART so get_toys matches the LAN-style tutorial. The timeout is scan listen time only, not how long motors run.
get_toys is synthetic (registration + connection state + optional UART battery / type hints), not the Lovense app’s own inventory.
MQTT bridge for Home Assistant (MQTT Discovery). Commands go through :class:~lovensepy.standard.async_base.LovenseAsyncControlClient — either :class:~lovensepy.standard.async_lan.AsyncLANClient (transport="lan") or :class:~lovensepy.ble_direct.hub.BleDirectHub (transport="ble"). Optional :class:~lovensepy.toy_events.client.ToyEventsClient is used only in LAN mode for live battery / strength when use_toy_events=True.
Requires paho-mqtt (pip install 'lovensepy[mqtt]'). BLE transport also needs bleak (pip install 'lovensepy[ble]').
Import: from lovensepy import HAMqttBridge (lazy-loaded) or from lovensepy.integrations.mqtt import HAMqttBridge.
HAMqttBridge(
mqtt_host: str,
mqtt_port: int = 1883,
*,
lan_ip: str | None = None,
transport: Literal["lan", "ble"] = "lan",
lan_port: int = 20011,
toy_events_port: int | None = None,
app_name: str = "lovensepy_ha",
topic_prefix: str = "lovensepy",
mqtt_username: str | None = None,
mqtt_password: str | None = None,
mqtt_client_id: str | None = None,
refresh_interval: float = 45.0,
use_https: bool = False,
use_toy_events: bool = True,
ble_discover_timeout: float = 15.0,
ble_name_prefix: str | None = "LVS-",
ble_enrich_uart: bool = True,
ble_client_kwargs: dict[str, Any] | None = None,
ble_hub: BleDirectHub | None = None,
)| Parameter | Description |
|---|---|
mqtt_host, mqtt_port |
MQTT broker (Home Assistant integration uses the same broker). |
transport |
"lan" (Game Mode HTTP) or "ble" (direct BLE hub). |
lan_ip, lan_port |
Required when transport="lan": Lovense Remote Game Mode HTTP API (/command). |
toy_events_port |
Toy Events WebSocket (default: same as lan_port, usually 20011). LAN only. |
topic_prefix |
Base prefix for state/command topics and discovery device grouping. |
use_toy_events |
LAN only: if False, only polling GetToys is used (no live battery/strength). Ignored when transport="ble". |
ble_discover_timeout |
BLE: scan listen time for discover_and_connect when ble_hub is not passed. |
ble_name_prefix, ble_enrich_uart, ble_client_kwargs |
Passed to :meth:~lovensepy.ble_direct.hub.BleDirectHub.discover_and_connect. |
ble_hub |
Optional pre-built hub (transport="ble" only); if omitted, the bridge creates one and runs discovery on start(). |
| Method / property | Description |
|---|---|
async start() |
Connect MQTT, open transport (LAN client or BLE scan/connect), subscribe, publish discovery, start refresh (+ Toy Events in LAN mode when enabled). |
async stop() |
Cancel tasks, publish offline, disconnect MQTT, aclose the control client. |
availability_topic |
Retained bridge status topic (e.g. lovensepy/bridge/status). Entities also use per-toy …/<safe_toy_id>/device_availability (see MQTT Discovery availability list). |
High-level API for sine waves and combo patterns.
For use with LANClient. Synchronous.
SyncPatternPlayer(client: LANClient, toys: dict[str, dict] | GetToysResponse)| Method | Parameters | Description |
|---|---|---|
play_sine_wave(toy_id, feature, duration_sec=5, num_steps=100, stop_prev_first=True) |
feature: e.g. "Vibrate1" |
Play sine wave on one feature. |
play_combo(targets, duration_sec=4, num_steps=100) |
targets: [(toy_id, feature), ...] |
Play combo with random phases. |
stop(toy_id) |
— | Stop toy. |
features(toy_id) |
— | Get features for toy. |
Example:
player = SyncPatternPlayer(client, toys)
player.play_sine_wave("T123", "Vibrate1", duration_sec=5)
player.play_combo([("T1", "Vibrate1"), ("T2", "Vibrate")], duration_sec=4)
player.stop("T123")For use with SocketAPIClient. Same methods, async (use await).
player = AsyncPatternPlayer(client, toys)
await player.play_sine_wave("T123", "Vibrate1", duration_sec=5)
await player.stop("T123")All types live in lovensepy.exceptions and are re-exported from lovensepy. Network-related subclasses attach endpoint (URL or logical name) and optional payload (the command dict sent, when useful for logging).
Hierarchy
LovenseError
└── LovenseNetworkError # .endpoint, .payload
├── LovenseAuthError # HTTP 401 / 403 (wrong token, forbidden)
├── LovenseDeviceOfflineError
│ ├── LovenseTimeoutError # HTTP client timeout (aiohttp)
│ └── LovenseBLEError # BLE / GATT, missing bleak, not connected
└── LovenseResponseParseError
How to tell errors apart (LAN / Server HTTP)
| Situation | Exception | Notes |
|---|---|---|
| Wrong or expired developer token, forbidden | LovenseAuthError |
From HttpTransport / async transport on status 401 or 403. |
| App unreachable, connection refused, DNS failure | LovenseDeviceOfflineError |
Connection errors from aiohttp (e.g. ClientConnectorError) — not the same as timeout. |
| Request took too long | LovenseTimeoutError |
Subclass of LovenseDeviceOfflineError — use isinstance(e, LovenseTimeoutError) before the parent if you want separate handling. |
| Other HTTP failures (non-200 not 401/403) | LovenseNetworkError |
Generic transport/HTTP issue. |
| Body is not valid JSON | LovenseResponseParseError |
BLE
Most failures are LovenseBLEError (subclass of LovenseDeviceOfflineError): not connected, unknown toy id on the hub, missing UART TX, bleak errors wrapped for GATT, and library-side timeouts (messages often contain timed out). There is no separate BLE auth type — pairing/security is outside this client.
Socket API
Command/send paths can raise LovenseDeviceOfflineError when the socket path treats the session as down; lower layers still use LovenseError in some cases. Prefer except LovenseError as a wide net, then narrow by type.
Handling pattern
Catch more specific types first, then LovenseNetworkError, then LovenseError:
from lovensepy import (
LovenseAuthError,
LovenseBLEError,
LovenseDeviceOfflineError,
LovenseError,
LovenseResponseParseError,
LovenseTimeoutError,
)
try:
await client.function_request({...}, time=1)
except LovenseAuthError:
# Refresh token / fix Dashboard credentials
...
except LovenseTimeoutError:
# HTTP took too long — retry or increase client timeout
...
except LovenseBLEError:
# Radio / GATT — reconnect, adjust gatt_write_max_attempts, check Remote not holding BLE
...
except LovenseResponseParseError:
# Unexpected JSON — log payload, report upstream change
...
except LovenseDeviceOfflineError:
# Reachability (includes BLE subclass unless caught above)
...
except LovenseError:
# Any other library error
...BLE resilience vs Socket auto_reconnect
- Socket API:
SocketAPIClient.start_background(auto_reconnect=True)keeps the session alive across disconnects (long-lived WebSocket). - BLE:
silence_on_link_lossonBleDirectClientperforms a one-shot reconnect only to send a stop burst after an unexpected drop; it does not replay your commands. - GATT writes: set
gatt_write_max_attempts>1(and optional delay kwargs) onBleDirectClientso individualwrite_gatt_charcalls retry on transientBleakError/ timeout / OS errors. Pass the same kwargs throughBleDirectHub.add_toy(..., **client_kwargs)ordiscover_and_connect(**client_kwargs). - Session lost: call
await client.connect()again, orawait hub.discover_and_connect(...)after a failure; wrap in your own backoff if the link is often flaky.
| Function | Parameters | Returns | Description |
|---|---|---|---|
get_token(developer_token, uid, uname=None, utoken=None, timeout=10) |
— | str | Get auth token for Socket API. Raises on error. |
get_socket_url(auth_token, platform, timeout=10) |
platform: Website Name from Dashboard |
dict | Get socket info dict. |
build_websocket_url(socket_info, auth_token) |
— | str | Build full wss:// URL. |
get_qr_code(developer_token, uid, uname=None, utoken=None, timeout=10) |
— | dict | Get QR for Server API. Returns {qr, code}. See security note in docstring. |
features_for_toy(toy) |
toy: dict from GetToys |
list[str] | Get features (e.g. ["Vibrate1", "Rotate"]). |
stop_actions(toy) |
toy: dict |
dict | Build {Vibrate1: 0, ...} to stop. |