rust_async_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_async_tuyapi;
9//! # use rust_async_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//! # async fn set_device() -> 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//! let dps = serde_json::to_value(&dps).unwrap();
23//!
24//! // Create the payload to be sent, this will be serialized to the JSON format
25//! let payload = Payload::Struct(PayloadStruct{
26//!        dev_id: "123456789abcdef".to_string(),
27//!        gw_id: Some("123456789abcdef".to_string()),
28//!        uid: None,
29//!        t: Some(current_time.to_string()),
30//!        dp_id: None,
31//!        dps: Some(dps),
32//!        });
33//! // Create a TuyaDevice, this is the type used to set/get status to/from a Tuya compatible
34//! // device.
35//! let mut tuya_device = TuyaDevice::new("3.3", "fedcba987654321", None,
36//!     IpAddr::from_str("192.168.0.123").unwrap())?;
37//!
38//! // Set the payload state on the Tuya device, an error here will contain
39//! // the error message received from the device.
40//! tuya_device.set(payload).await?;
41//! # Ok(())
42//! # }
43//! ```
44mod cipher;
45mod crc;
46pub mod error;
47pub mod mesparse;
48pub mod tuyadevice;
49
50extern crate num;
51extern crate num_derive;
52#[macro_use]
53extern crate lazy_static;
54
55use serde::{Deserialize, Serialize};
56
57use std::convert::TryFrom;
58use std::fmt::Display;
59
60use crate::error::ErrorKind;
61use std::convert::TryInto;
62
63pub type Result<T> = std::result::Result<T, ErrorKind>;
64/// The Payload enum represents a payload sent to, and recevied from the Tuya devices. It might be
65/// a struct (ser/de from json) or a plain string.
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub enum Payload {
68    Struct(PayloadStruct),
69    ControlNewStruct(ControlNewPayload),
70    String(String),
71    Raw(Vec<u8>),
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub enum DpId {
76    Lower,
77    Higher,
78}
79
80impl DpId {
81    fn get_ids(self) -> Vec<u8> {
82        match self {
83            DpId::Lower => vec![4, 5, 6],
84            DpId::Higher => vec![18, 19, 20],
85        }
86    }
87}
88
89impl Payload {
90    pub fn new(
91        dev_id: String,
92        gw_id: Option<String>,
93        uid: Option<String>,
94        t: Option<u32>,
95        dp_id: Option<DpId>,
96        dps: Option<serde_json::Value>,
97    ) -> Payload {
98        Payload::Struct(PayloadStruct {
99            dev_id,
100            gw_id,
101            uid,
102            t: t.map(|t| t.to_string()),
103            dp_id: dp_id.map(DpId::get_ids),
104            dps,
105        })
106    }
107}
108
109impl Display for Payload {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        match self {
112            Payload::Struct(s) => write!(f, "{}", s),
113            Payload::ControlNewStruct(s) => write!(f, "{}", s),
114            Payload::String(s) => write!(f, "{}", s),
115            Payload::Raw(s) => write!(f, "{}", hex::encode(s)),
116        }
117    }
118}
119
120/// The PayloadStruct is Serialized to json and sent to the device. The dps field contains the
121/// actual commands to set and are device specific.
122#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
123pub struct PayloadStruct {
124    #[serde(rename = "gwId", skip_serializing_if = "Option::is_none")]
125    pub gw_id: Option<String>,
126    #[serde(rename = "devId")]
127    pub dev_id: String,
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub uid: Option<String>,
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub t: Option<String>,
132    #[serde(rename = "dpId", skip_serializing_if = "Option::is_none")]
133    pub dp_id: Option<Vec<u8>>,
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub dps: Option<serde_json::Value>,
136}
137
138/// Protocol v3.4 uses different payloads for ControlNew commands, for example
139/// {"protocol":5,"t":1,"data":{"dps":{"20":false}}}
140#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
141pub struct ControlNewPayload {
142    pub protocol: u32,
143    pub t: u32,
144    pub data: ControlNewPayloadData,
145}
146
147#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
148pub struct ControlNewPayloadData {
149    dps: serde_json::Value,
150}
151/// This trait is implemented to allow truncated logging of secret data.
152pub trait Truncate {
153    fn truncate(&self) -> Self;
154
155    /// Take the last 5 characters
156    fn truncate_str(text: &str) -> &str {
157        if let Some((i, _)) = text.char_indices().rev().nth(5) {
158            return &text[i..];
159        }
160        text
161    }
162}
163
164impl TryFrom<Vec<u8>> for Payload {
165    type Error = ErrorKind;
166
167    fn try_from(vec: Vec<u8>) -> Result<Self> {
168        match serde_json::from_slice(&vec)? {
169            serde_json::Value::String(s) => Ok(Payload::String(s)),
170            value => Ok(Payload::Struct(serde_json::from_value(value)?)),
171        }
172    }
173}
174impl TryInto<Vec<u8>> for Payload {
175    type Error = ErrorKind;
176
177    fn try_into(self) -> Result<Vec<u8>> {
178        match self {
179            Payload::Struct(s) => Ok(serde_json::to_vec(&s)?),
180            Payload::ControlNewStruct(s) => Ok(serde_json::to_vec(&s)?),
181            Payload::String(s) => Ok(s.as_bytes().to_vec()),
182            Payload::Raw(s) => Ok(s),
183        }
184    }
185}
186
187impl Truncate for PayloadStruct {
188    fn truncate(&self) -> PayloadStruct {
189        PayloadStruct {
190            dev_id: String::from("...") + Self::truncate_str(&self.dev_id),
191            gw_id: self
192                .gw_id
193                .as_ref()
194                .map(|gwid| String::from("...") + Self::truncate_str(gwid)),
195            t: self.t.clone(),
196            dp_id: self.dp_id.clone(),
197            uid: self.uid.clone(),
198            dps: self.dps.clone(),
199        }
200    }
201}
202
203impl Display for PayloadStruct {
204    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205        let full_display = std::env::var("TUYA_FULL_DISPLAY").map_or_else(|_| false, |_| true);
206        if full_display {
207            write!(f, "{}", serde_json::to_string(self).unwrap())
208        } else {
209            write!(f, "{}", serde_json::to_string(&self.truncate()).unwrap())
210        }
211    }
212}
213
214impl Display for ControlNewPayload {
215    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216        write!(f, "{}", serde_json::to_string(self).unwrap())
217    }
218}