12 releases (breaking)
Uses new Rust 2024
| new 0.10.0 | Jun 5, 2026 |
|---|---|
| 0.8.0 | Jun 4, 2026 |
| 0.5.0 | Jan 9, 2026 |
| 0.3.0 | Dec 31, 2025 |
#4 in Network programming
655KB
12K
SLoC
TasmoR Lib
Primary repository: Codeberg — Please submit issues and pull requests there.
A modern, type-safe Rust library for controlling Tasmota IoT devices via MQTT and HTTP protocols.
Early Development: This project is in active development (v0.x.x). The API may change between versions. Not recommended for production use yet.
Tested with: Tasmota firmware v15.2.0
Features
- Type-safe API - Compile-time guarantees for valid commands and values
- Dual protocol support - Control devices via MQTT or HTTP with full feature parity
- Async/await - Built on Tokio for efficient async I/O
- Full device support - Lights (RGB/CCT), switches, relays, energy monitors
- Event-driven architecture - Subscribe to device state changes in real-time (MQTT)
- Well-tested - Comprehensive unit and integration tests (630+ tests)
Supported Capabilities
| Capability | Description |
|---|---|
| Power control | On/Off/Toggle for single and multi-relay devices |
| Lighting | Dimmer, color temperature (CCT), HSB color control |
| Energy monitoring | Power, voltage, current, energy consumption tracking |
| Device status | Query firmware, network, and sensor information |
| Transitions | Fade effects and duration control |
| Light schemes | Effects (wakeup, color cycling, random) |
| RGB colors | Hex color input (#RRGGBB) with HSB conversion |
| Routines | Execute multiple commands atomically via Backlog0 |
| Device discovery | Auto-discover Tasmota devices on MQTT broker |
Installation
Add to your Cargo.toml:
[dependencies]
tasmor_lib = "0.10"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
System Requirements
The MQTT feature (enabled by default) links against the Eclipse Paho MQTT C library, which is compiled from source during cargo build.
Build-time dependencies (required when the mqtt feature is enabled):
| Dependency | Purpose | Install |
|---|---|---|
C compiler (cc) |
Compile paho.mqtt.c | Usually pre-installed |
cmake ≥ 3.x |
Build system for paho.mqtt.c | apt install cmake / brew install cmake / pacman -S cmake |
Runtime dependencies (required in the final binary):
| Library | Purpose |
|---|---|
libssl + libcrypto (OpenSSL) |
TLS support in paho.mqtt.c |
OpenSSL is present on most Linux/macOS systems. For minimal Docker images (e.g., debian:slim, alpine), add it explicitly:
# Debian/Ubuntu
RUN apt-get install -y libssl3
# Alpine
RUN apk add --no-cache openssl-libs-static
If you only use HTTP (default-features = false, features = ["http"]), none of these system dependencies apply.
Feature Flags
Both HTTP and MQTT protocols are enabled by default. To reduce compile time and binary size, you can enable only the protocol you need:
# HTTP only (no C dependencies, no OpenSSL requirement)
tasmor_lib = { version = "0.10", default-features = false, features = ["http"] }
# MQTT only (no HTTP dependencies)
tasmor_lib = { version = "0.10", default-features = false, features = ["mqtt"] }
Quick Start
Basic Switch Control
The simplest use case - controlling a smart switch or relay:
use tasmor_lib::{Device, Capabilities};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Connect to a Tasmota switch via HTTP
let (device, initial_state) = Device::http("192.168.1.100")
.with_capabilities(Capabilities::basic())
.build_without_probe()
.await?;
// Check current state
println!("Power is {:?}", initial_state.power(1));
// Toggle power and get the response
let response = device.power_toggle().await?;
println!("Power is now {:?}", response.power_state(1));
Ok(())
}
MQTT Connection
For persistent connections with real-time updates:
use tasmor_lib::{MqttBroker, Capabilities};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Connect to MQTT broker
let broker = MqttBroker::builder()
.host("192.168.1.50")
.credentials("mqtt_user", "mqtt_password")
.build()
.await?;
// Create device from broker
let (device, initial_state) = broker.device("tasmota_switch")
.with_capabilities(Capabilities::basic())
.build_without_probe()
.await?;
println!("Power is {:?}", initial_state.power(1));
device.power_on().await?;
// Clean disconnect when done
device.disconnect().await;
broker.disconnect().await?;
Ok(())
}
Building Devices
build() vs build_without_probe()
Both methods return (Device, DeviceState) - the device handle and its initial state.
| Method | When to use |
|---|---|
build() |
Auto-detects device capabilities by querying device status. Use when you don't know the device type. |
build_without_probe() |
Uses capabilities you provide. Faster startup, recommended when you know the device type. |
// Auto-detection: queries the device to discover capabilities
let (device, state) = Device::http("192.168.1.100")
.build()
.await?;
// Manual: you specify the capabilities (no capability query)
let (device, state) = Device::http("192.168.1.100")
.with_capabilities(Capabilities::rgbcct_light())
.build_without_probe()
.await?;
Both methods query the device for its current state (power, dimmer, energy, etc.) and return it as DeviceState.
Predefined Capabilities
use tasmor_lib::Capabilities;
Capabilities::basic() // Simple switch (1 relay)
Capabilities::neo_coolcam() // Smart plug with energy monitoring
Capabilities::rgbcct_light() // Full RGB + CCT light bulb
Capabilities::rgb_light() // RGB only light
Capabilities::cct_light() // Color temperature only light
Custom Capabilities
use tasmor_lib::CapabilitiesBuilder;
let caps = CapabilitiesBuilder::new()
.with_power_channels(2) // Dual relay
.with_dimmer_control()
.with_energy_monitoring()
.build();
Examples by Use Case
Light Control (RGB/CCT)
use tasmor_lib::{Device, Capabilities, ColorTemperature, Dimmer, HsbColor};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let (device, _) = Device::http("192.168.1.100")
.with_capabilities(Capabilities::rgbcct_light())
.build_without_probe()
.await?;
// Power and brightness
device.power_on().await?;
device.set_dimmer(Dimmer::new(75)?).await?;
// Color temperature (warm to cold)
device.set_color_temperature(ColorTemperature::WARM).await?;
// RGB color
device.set_hsb_color(HsbColor::blue()).await?;
// Custom HSB color (hue: 0-360, saturation: 0-100, brightness: 0-100)
device.set_hsb_color(HsbColor::new(120, 80, 100)?).await?;
Ok(())
}
Multi-Relay Control
For devices with multiple relays (e.g., dual switch):
use tasmor_lib::{Device, CapabilitiesBuilder, PowerIndex, PowerState};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let caps = CapabilitiesBuilder::new()
.with_power_channels(2)
.build();
let (device, state) = Device::http("192.168.1.100")
.with_capabilities(caps)
.build_without_probe()
.await?;
// Check each relay
println!("Relay 1: {:?}", state.power(1));
println!("Relay 2: {:?}", state.power(2));
// Control individual relays
device.set_power(PowerIndex::new(1)?, PowerState::On).await?;
device.set_power(PowerIndex::new(2)?, PowerState::Off).await?;
// Toggle a specific relay
device.toggle_power(PowerIndex::new(2)?).await?;
Ok(())
}
Energy Monitoring
use tasmor_lib::{Device, Capabilities};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let (device, state) = Device::http("192.168.1.101")
.with_capabilities(Capabilities::neo_coolcam())
.build_without_probe()
.await?;
// Energy data is available in initial state
println!("Power: {:?} W", state.power_consumption());
println!("Voltage: {:?} V", state.voltage());
println!("Current: {:?} A", state.current());
println!("Today: {:?} kWh", state.energy_today());
println!("Yesterday: {:?} kWh", state.energy_yesterday());
println!("Total: {:?} kWh", state.energy_total());
// Reset total energy counter
let updated = device.reset_energy_total().await?;
if let Some(energy) = updated.energy() {
println!("Reset! New total: {} kWh", energy.total);
}
Ok(())
}
Typed Responses
All commands return typed responses for reliable parsing:
use tasmor_lib::{Device, Capabilities, Dimmer};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let (device, _) = Device::http("192.168.1.100")
.with_capabilities(Capabilities::rgbcct_light())
.build_without_probe()
.await?;
// PowerResponse
let power_resp = device.power_on().await?;
println!("Relay 1 state: {:?}", power_resp.power_state(1));
println!("All relays: {:?}", power_resp.all_power_states());
// DimmerResponse
let dimmer_resp = device.set_dimmer(Dimmer::new(50)?).await?;
println!("Dimmer level: {}", dimmer_resp.dimmer());
// ColorTemperatureResponse
let ct_resp = device.set_color_temperature(153u16.try_into()?).await?;
println!("Color temp: {} mireds", ct_resp.color_temperature());
// HsbColorResponse
let hsb_resp = device.set_hsb_color((180u16, 100u8, 100u8).try_into()?).await?;
println!("HSB: {:?}", hsb_resp.hsb_color());
Ok(())
}
Real-Time Updates (MQTT Callbacks)
Subscribe to device state changes pushed via MQTT:
use tasmor_lib::{MqttBroker, Capabilities};
use tasmor_lib::subscription::Subscribable;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let broker = MqttBroker::builder()
.host("192.168.1.50")
.credentials("mqtt_user", "mqtt_pass")
.build()
.await?;
let (device, _) = broker.device("tasmota_bulb")
.with_capabilities(Capabilities::rgbcct_light())
.build_without_probe()
.await?;
// Subscribe to power changes (triggered by external events)
device.on_power_changed(|relay_index, power_state| {
println!("Relay {} changed to {:?}", relay_index, power_state);
});
// Subscribe to dimmer changes
device.on_dimmer_changed(|dimmer| {
println!("Dimmer changed to {}", dimmer.value());
});
// Subscribe to all state changes
device.on_state_changed(|change| {
println!("State change: {:?}", change);
});
// Handle connection events
device.on_disconnected(|| {
println!("Connection lost!");
});
device.on_reconnected(|| {
println!("Reconnected! Consider calling query_state() to refresh");
});
// Keep the application running to receive callbacks
tokio::signal::ctrl_c().await?;
device.disconnect().await;
broker.disconnect().await?;
Ok(())
}
Multi-Device Management
Multiple devices can share a single broker connection for efficiency:
use tasmor_lib::{MqttBroker, Capabilities, Dimmer};
use tasmor_lib::subscription::Subscribable;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Connect to broker once
let broker = MqttBroker::builder()
.host("192.168.1.50")
.credentials("mqtt_user", "mqtt_pass")
.build()
.await?;
// Create multiple devices sharing the broker connection
let (living_room, living_state) = broker.device("tasmota_living")
.with_capabilities(Capabilities::rgbcct_light())
.build_without_probe()
.await?;
let (bedroom, bedroom_state) = broker.device("tasmota_bedroom")
.with_capabilities(Capabilities::rgbcct_light())
.build_without_probe()
.await?;
// Initial states are immediately available
println!("Living room: {:?}", living_state.power(1));
println!("Bedroom: {:?}", bedroom_state.power(1));
// Subscribe to changes on each device
living_room.on_power_changed(|relay, state| {
println!("Living room relay {} -> {:?}", relay, state);
});
bedroom.on_power_changed(|relay, state| {
println!("Bedroom relay {} -> {:?}", relay, state);
});
// Control devices
living_room.power_on().await?;
living_room.set_dimmer(Dimmer::new(75)?).await?;
bedroom.power_on().await?;
// Clean disconnect
living_room.disconnect().await;
bedroom.disconnect().await;
broker.disconnect().await?;
Ok(())
}
MQTT Device Discovery
Automatically discover all Tasmota devices on an MQTT broker:
use tasmor_lib::MqttBroker;
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Connect to broker
let broker = MqttBroker::builder()
.host("192.168.1.50")
.port(1883)
.credentials("mqtt_user", "mqtt_pass")
.build()
.await?;
// Discover devices (10 second timeout)
let devices = broker.discover_devices(Duration::from_secs(10)).await?;
println!("Found {} devices:", devices.len());
for (device, state) in &devices {
println!(" - Power: {:?}, Dimmer: {:?}", state.power(1), state.dimmer());
}
// Use discovered devices...
for (device, _) in devices {
device.power_toggle().await?;
}
broker.disconnect().await?;
Ok(())
}
Command Routines
Execute multiple commands atomically using the Routine builder:
use std::time::Duration;
use tasmor_lib::{Device, Capabilities, Routine, Dimmer, HsbColor};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let (device, _) = Device::http("192.168.1.100")
.with_capabilities(Capabilities::rgbcct_light())
.build_without_probe()
.await?;
// Build a routine with multiple commands
let wakeup_routine = Routine::builder()
.set_dimmer(Dimmer::new(10)?)
.power_on()
.delay(Duration::from_secs(5))
.set_dimmer(Dimmer::new(50)?)
.delay(Duration::from_secs(5))
.set_dimmer(Dimmer::new(100)?)
.build()?;
// Execute all commands atomically
device.run(&wakeup_routine).await?;
// RGB color transition routine
let color_routine = Routine::builder()
.set_hsb_color(HsbColor::red())
.delay(Duration::from_secs(2))
.set_hsb_color(HsbColor::green())
.delay(Duration::from_secs(2))
.set_hsb_color(HsbColor::blue())
.build()?;
device.run(&color_routine).await?;
Ok(())
}
Parsing External Telemetry
If you're using your own MQTT client and want to parse Tasmota messages:
use tasmor_lib::telemetry::{parse_telemetry, TelemetryMessage};
fn handle_mqtt_message(topic: &str, payload: &str) {
if let Ok(msg) = parse_telemetry(topic, payload) {
match msg {
TelemetryMessage::State { device_topic, state } => {
println!("[{}] Power: {:?}, Dimmer: {:?}",
device_topic, state.power(), state.dimmer());
}
TelemetryMessage::Sensor { device_topic, data } => {
if let Some(energy) = data.energy() {
println!("[{}] Power: {} W", device_topic, energy.power);
}
}
TelemetryMessage::LastWill { device_topic, online } => {
println!("[{}] {}", device_topic, if online { "online" } else { "offline" });
}
_ => {}
}
}
}
Runnable Examples
The examples/ directory contains complete runnable examples:
| Example | Description |
|---|---|
bulb_test.rs |
Basic light bulb control |
energy_test.rs |
Energy monitoring with formatted output |
routine_test.rs |
Wakeup routine with gradual brightness increase |
discovery_test.rs |
MQTT device discovery |
uptime.rs |
Device uptime retrieval via HTTP, MQTT, and subscriptions |
cargo run --example bulb_test -- 192.168.1.50 tasmota_topic user pass
cargo run --example energy_test -- 192.168.1.50 tasmota_plug user pass
cargo run --example routine_test -- 192.168.1.50 tasmota_bulb user pass
cargo run --example discovery_test -- 192.168.1.50 user pass
cargo run --example uptime -- http 192.168.1.100 admin password
cargo run --example uptime -- mqtt 192.168.1.50 tasmota_device user pass
Documentation
- API Documentation - Full API reference
- Tasmota Commands Reference - Official Tasmota protocol
Roadmap
- Stabilize API for 1.0 release
Development
# Run tests
cargo test
# Run tests with serde feature
cargo test --features serde
# Check code coverage
cargo tarpaulin --out Stdout
# Full verification pipeline
cargo check && cargo build && cargo test && cargo fmt --check && cargo clippy -- -D warnings -W clippy::pedantic
Contributing
Contributions are welcome! Please read our Contributing Guide for details on:
- Development setup
- Code style and linting
- Testing requirements
- Commit message conventions
- Pull request process
License
Licensed under the Mozilla Public License 2.0 (MPL-2.0).
Credits
Built for controlling Tasmota open-source firmware.
Key dependencies:
Testing:
- wiremock - HTTP mocking
Dependencies
~8–20MB
~193K SLoC