rust_tuyapi/
lib.rs

1//! # Rust Tuyapi
2//! This library can be used to interact with Tuya/Smart Home devices. It utilizes the Tuya
3//! protocol version 3.1 and 3.3 to send and receive messages from the devices.
4//!
5//! ## Example
6//! This shows how to turn on a wall socket.
7//! ```no_run
8//! # extern crate rust_tuyapi;
9//! # use rust_tuyapi::{Payload, Result, PayloadStruct,tuyadevice::TuyaDevice};
10//! # use std::net::IpAddr;
11//! # use std::str::FromStr;
12//! # use std::collections::HashMap;
13//! # use std::time::SystemTime;
14//! # use serde_json::json;
15//! # fn main() -> Result<()> {
16//! // The dps value is device specific, this socket turns on with key "1"
17//! let mut dps = HashMap::new();
18//! dps.insert("1".to_string(), json!(true));
19//! let current_time = SystemTime::now()
20//!     .duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as u32;
21//!
22//! // Create the payload to be sent, this will be serialized to the JSON format
23//! let payload = Payload::Struct(PayloadStruct{
24//!        dev_id: "123456789abcdef".to_string(),
25//!        gw_id: Some("123456789abcdef".to_string()),
26//!        uid: None,
27//!        t: Some(current_time),
28//!        dp_id: None,
29//!        dps: Some(dps),
30//!        });
31//! // Create a TuyaDevice, this is the type used to set/get status to/from a Tuya compatible
32//! // device.
33//! let tuya_device = TuyaDevice::create("ver3.3", Some("fedcba987654321"),
34//!     IpAddr::from_str("192.168.0.123").unwrap())?;
35//!
36//! // Set the payload state on the Tuya device, an error here will contain
37//! // the error message received from the device.
38//! tuya_device.set(payload, 0)?;
39//! # Ok(())
40//! # }
41//! ```
42mod cipher;
43mod crc;
44pub mod error;
45pub mod mesparse;
46pub mod transports;
47pub mod tuyadevice;
48
49extern crate num;
50extern crate num_derive;
51#[macro_use]
52extern crate lazy_static;
53
54use serde::{Deserialize, Serialize};
55
56use std::collections::HashMap;
57use std::convert::TryFrom;
58use std::fmt::Display;
59
60use crate::error::ErrorKind;
61use std::convert::TryInto;
62
63pub use transports::Transport;
64pub use tuyadevice::TuyaDevice;
65
66pub type Result<T> = std::result::Result<T, ErrorKind>;
67/// The Payload enum represents a payload sent to, and recevied from the Tuya devices. It might be
68/// a struct (ser/de from json) or a plain string.
69#[derive(Debug, Clone, PartialEq)]
70pub enum Payload {
71    Struct(PayloadStruct),
72    String(String),
73}
74
75#[derive(Debug, Clone, PartialEq)]
76pub enum DpId {
77    Lower,
78    Higher,
79}
80
81impl DpId {
82    fn get_ids(self) -> Vec<u8> {
83        match self {
84            DpId::Lower => vec![4, 5, 6],
85            DpId::Higher => vec![18, 19, 20],
86        }
87    }
88}
89
90impl Payload {
91    pub fn new(
92        dev_id: String,
93        gw_id: Option<String>,
94        uid: Option<String>,
95        t: Option<u32>,
96        dp_id: Option<DpId>,
97        dps: Option<HashMap<String, serde_json::Value>>,
98    ) -> Payload {
99        Payload::Struct(PayloadStruct {
100            dev_id,
101            gw_id,
102            uid,
103            t,
104            dp_id: dp_id.map(DpId::get_ids),
105            dps,
106        })
107    }
108}
109
110impl Display for Payload {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        match self {
113            Payload::Struct(s) => write!(f, "{}", s),
114            Payload::String(s) => write!(f, "{}", s),
115        }
116    }
117}
118
119/// The PayloadStruct is Serialized to json and sent to the device. The dps field contains the
120/// actual commands to set and are device specific.
121#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
122pub struct PayloadStruct {
123    #[serde(rename = "devId")]
124    pub dev_id: String,
125    #[serde(rename = "gwId", skip_serializing_if = "Option::is_none")]
126    pub gw_id: Option<String>,
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub uid: Option<String>,
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub t: Option<u32>,
131    #[serde(rename = "dpId", skip_serializing_if = "Option::is_none")]
132    pub dp_id: Option<Vec<u8>>,
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub dps: Option<HashMap<String, serde_json::Value>>,
135}
136
137/// This trait is implemented to allow truncated logging of secret data.
138pub trait Truncate {
139    fn truncate(&self) -> Self;
140
141    /// Take the last 5 characters
142    fn truncate_str(text: &str) -> &str {
143        if let Some((i, _)) = text.char_indices().rev().nth(5) {
144            return &text[i..];
145        }
146        text
147    }
148}
149
150impl TryFrom<Vec<u8>> for Payload {
151    type Error = ErrorKind;
152
153    fn try_from(vec: Vec<u8>) -> Result<Self> {
154        match serde_json::from_slice(&vec)? {
155            serde_json::Value::String(s) => Ok(Payload::String(s)),
156            value => Ok(Payload::Struct(serde_json::from_value(value)?)),
157        }
158    }
159}
160impl TryInto<Vec<u8>> for Payload {
161    type Error = ErrorKind;
162
163    fn try_into(self) -> Result<Vec<u8>> {
164        match self {
165            Payload::Struct(s) => Ok(serde_json::to_vec(&s)?),
166            Payload::String(s) => Ok(s.as_bytes().to_vec()),
167        }
168    }
169}
170
171impl Truncate for PayloadStruct {
172    fn truncate(&self) -> PayloadStruct {
173        PayloadStruct {
174            dev_id: String::from("...") + Self::truncate_str(&self.dev_id),
175            gw_id: self
176                .gw_id
177                .as_ref()
178                .map(|gwid| String::from("...") + Self::truncate_str(gwid)),
179            t: self.t,
180            dp_id: self.dp_id.clone(),
181            uid: self.uid.clone(),
182            dps: self.dps.clone(),
183        }
184    }
185}
186
187impl Display for PayloadStruct {
188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189        let full_display = std::env::var("TUYA_FULL_DISPLAY").map_or_else(|_| false, |_| true);
190        if full_display {
191            write!(f, "{}", serde_json::to_string(self).unwrap())
192        } else {
193            write!(f, "{}", serde_json::to_string(&self.truncate()).unwrap())
194        }
195    }
196}