Skip to the content.

Migrating from rustuya 0.3 to 0.4

0.4 is a breaking, pure-Rust redesign. It requires Rust 1.89+. Python bindings and the blocking/sync API remain on 0.3.x; there is no 0.4 replacement for those interfaces yet. The previous master is preserved on the 0.3-stable branch; master now tracks 0.4. Embassy/ESP32 support is planned for 0.5+.

Dependencies and imports

[dependencies]
rustuya = { version = "0.4", features = ["tokio"] }
tokio = { version = "1", features = ["full"] }

Import driver types from rustuya::tokio. The facade has no default driver. For an embedded implementation, depend on rustuya-core directly; it contains the no_std + alloc state machines, but does not provide sockets or a runtime.

Commands and responses

Commands return after entering the bounded queue. Success does not mean the device received or acknowledged the command. The protocol does not reliably correlate responses with requests, so responses and pushes share an event stream.

use rustuya::tokio::{Device, Event, Version};

#[tokio::main]
async fn main() -> rustuya::tokio::Result<()> {
let dev = Device::builder("device_id_22chars0000", "0123456789abcdef")
    .address("192.168.1.50")
    .version(Version::V3_4)
    .connect()?;
let mut events = dev.listener(); // Subscribe before sending.
dev.query().await?;
while let Some(event) = events.recv().await {
    match event {
        Event::Frame(frame) => println!("{frame:?}"),
        Event::Lagged(skipped) => {
            eprintln!("missed {skipped} frames; requesting fresh status");
            dev.query().await?;
        }
    }
}
Ok(())
}
0.3 concept 0.4 replacement
Top-level async device API rustuya::tokio::{Device, Discovery, ...}
status() returning a response query() -> Result<()>, then consume listener()
request / request_message send(cmd, data) -> Result<()>
set_dps / set_value returning replies Result<()>; acknowledgements arrive on the listener
Global scanner Explicit shared Discovery
Addressless construction Device::builder(id, key).discover(&disco, timeout).await
Global connection concurrency cap Shared ConnectLimiter passed to builders
Unified listener MultiListener with explicit device membership
Python / sync wrapper Stay on 0.3.x

watch_status() retains the last non-empty frame; it does not merge partial DPS updates. Pair it with watch_connected() to judge whether that value is stale. Connection transitions are separate from frame events. Use watch_error() for authentication failures and wait_connected(timeout) when startup must complete.

Timeouts, shutdown and discovery

send_timeout bounds waiting for both connection and command-queue capacity; it is not a device-response timeout. Put your own deadline around awaiting an event when your application needs one.

close().await stops the actor and waits for it to exit. It discards queued commands and partial writes; await the application-level result you need before closing. Dropping the last device handle also stops the actor.

Share one Discovery across devices. .discover() links it for later IP changes and reconnect wakes; for a known IP, use .rediscover(&disco). For immediate addressless registration with background retry (0.4.5+), use .rediscover(&disco).connect() without an address. As of 0.4.6, Version::Auto also requires discovery and waits for a concrete announced version even with a configured IP. Set an explicit version to connect without discovery; explicit versions are not replaced by announcements. Discovery checks the announced IP against the UDP source by default. If an intentional relay changes that source, configure require_source_match(false) explicitly. max_devices bounds the discovery cache.