1use std::collections::HashMap;
7use std::ffi::CString;
8
9use pyo3::prelude::*;
10use pyo3::types::{PyBytes, PyDict, PyModule};
11
12use serde::{Deserialize, Serialize};
13use serde_json;
14
15mod constants;
16
17const MIIO_INTERFACE_CODE: &str = include_str!("../python-src/miio_interface.py");
18
19pub fn get_device_types() -> Result<Vec<String>, PyErr> {
27 Python::with_gil(|py| {
28 let miio_module = PyModule::from_code(
30 py,
31 CString::new(MIIO_INTERFACE_CODE)?.as_c_str(),
32 &CString::new("miio_interface.py")?,
33 &CString::new("miio_interface")?,
34 )?;
35
36 let get_device_types = miio_module.getattr("get_device_types")?;
38 let device_types_py = get_device_types.call0()?;
40 let v: Vec<String> = device_types_py.extract()?;
42 Ok(v)
43 })
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct Device {
52 pub serialized_py_object: Vec<u8>,
54 pub callable_methods: HashMap<String, String>,
56}
57
58impl Device {
59 pub fn deserialize_json(json_str: &str) -> Result<Device, serde_json::Error> {
74 serde_json::from_str(json_str)
75 }
76
77 pub fn serialize_json(&self) -> Result<String, serde_json::Error> {
90 serde_json::to_string_pretty(self)
91 }
92
93 pub fn serialize_to_file(&self, folder: &str, file_name: &str) -> std::io::Result<()> {
104 let path = format!("{}/{}", folder, file_name);
105 let json_str = self.serialize_json()?;
106 std::fs::write(path, json_str)
107 }
108
109 pub fn deserialize_from_file(folder: &str, file_name: &str) -> std::io::Result<Device> {
121 let path = format!("{}/{}", folder, file_name);
122 let json_str = std::fs::read_to_string(path)?;
123 Ok(Device::deserialize_json(&json_str)?)
124 }
125
126 pub fn create_device(ip: &str, token: &str, device_type: &str) -> Result<Device, PyErr> {
142 Python::with_gil(|py| {
143 let miio_module = PyModule::from_code(
145 py,
146 CString::new(MIIO_INTERFACE_CODE)?.as_c_str(),
147 &CString::new("miio_interface.py")?,
148 &CString::new("miio_interface")?,
149 )?;
150
151 let create_device = miio_module.getattr("get_device")?;
153 let device: Bound<'_, PyBytes> = create_device
155 .call1((ip, token, device_type))?
156 .downcast::<PyBytes>()?
157 .clone();
158
159 let get_device_methods = miio_module.getattr("get_device_methods")?;
161 let methods = get_device_methods.call1((device.clone(),))?; let methods = methods.downcast::<PyDict>()?;
164 let mut callable_methods = HashMap::new();
165 for (key, value) in methods.iter() {
166 let key = key.extract::<String>()?;
167 let value = value.extract::<String>()?;
168 callable_methods.insert(key, value);
169 }
170
171 let device_bytes = device.as_bytes().to_vec();
172 Ok(Device {
173 serialized_py_object: device_bytes,
174 callable_methods,
175 })
176 })
177 }
178
179 pub fn call_method(&self, method_name: &str, args: Vec<&str>) -> Result<String, PyErr> {
194 Python::with_gil(|py| {
195 let miio_module = PyModule::from_code(
197 py,
198 CString::new(MIIO_INTERFACE_CODE)?.as_c_str(),
199 &CString::new("miio_interface.py")?,
200 &CString::new("miio_interface")?,
201 )?;
202
203 let call_method = miio_module.getattr("call_method")?;
205 let result: String = call_method
207 .call1((self.serialized_py_object.clone(), method_name, args))?
208 .extract()?;
209 Ok(result)
210 })
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::constants::*;
217 use super::*;
218
219 #[test]
220 fn test_python_path() {
221 let res: Result<(), PyErr> = Python::with_gil(|py| {
223 let sys = PyModule::import(py, "sys")?;
224 let path = sys.getattr("path")?;
225 let path: Vec<String> = path.extract()?;
226 if path.is_empty() {
227 return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
228 "Python path is empty",
229 ));
230 }
231 Ok(())
232 });
233 assert!(res.is_ok());
234 }
235
236 #[test]
237 fn test_get_device_types_success() {
238 assert!(!get_device_types().unwrap().is_empty())
239 }
240
241 #[test]
242 fn test_get_device_types_cherry_picked() {
243 let device_types = get_device_types().unwrap();
244 assert!(device_types.contains(&String::from("Yeelight")));
245 assert!(device_types.contains(&String::from("FanMiot")));
246 assert!(device_types.contains(&String::from("AirHumidifierMiot")));
247 }
248
249 #[test]
250 fn test_get_device_types_cherry_picked_ne() {
251 let device_types = get_device_types().unwrap();
252 assert!(!device_types.contains(&String::from("Yeeli")));
253 assert!(!device_types.contains(&String::from("DummyStupidWifiRepeater")));
254 assert!(!device_types.contains(&String::from("DummySleepingpad")));
255 assert!(!device_types.contains(&String::from("Fanatico")));
256 assert!(!device_types.contains(&String::from("Yourmama")));
257 }
258
259 #[test]
260 fn test_create_device_success() {
261 let device = Device::create_device(IP, TOKEN, DEVICE_TYPE).unwrap();
262 assert!(!device.serialized_py_object.is_empty());
263 }
264
265 #[test]
266 fn test_get_device_types_error() {
267 assert!(!Device::create_device("0.0.0.0.0.0.0.0.0", TOKEN, DEVICE_TYPE,).is_ok());
268 assert!(!Device::create_device(IP, "tokennnnnnnnnnnnnn", DEVICE_TYPE,).is_ok());
269 assert!(!Device::create_device(IP, TOKEN, "NotADeviceYkkkkkkkkkkkkk",).is_ok());
270 }
271
272 #[test]
273 fn test_get_device_methods() {
274 let device = Device::create_device(IP, TOKEN, DEVICE_TYPE).unwrap();
275 assert!(!device.callable_methods.is_empty());
276 assert!(device.callable_methods.contains_key("toggle"));
277 }
278
279 #[test]
280 fn test_call_method() {
281 let device = Device::create_device(IP, TOKEN, DEVICE_TYPE).unwrap();
282 let result = device.call_method(METHOD_NAME, vec![]).unwrap();
283 assert_eq!(result, "['ok']");
284 let device = Device::create_device(IP, TOKEN, DEVICE_TYPE).unwrap();
285 let result = device.call_method("set_rgb", vec!["(32, 32, 32)"]).unwrap();
286 assert_eq!(result, "['ok']");
287 }
288
289 #[test]
290 fn test_call_method_err() {
291 let device = Device::create_device(IP, TOKEN, DEVICE_TYPE).unwrap();
292 let result = device.call_method("set_rgb", vec!["[32, 32, 32]"]).unwrap();
293 eprintln!("{}", result);
294 }
295
296 #[test]
297 fn test_serialize_to_file() {
298 let device = Device::create_device(IP, TOKEN, DEVICE_TYPE).unwrap();
299 let folder = std::env::temp_dir();
300 let folder = folder.to_str().unwrap();
301 let file_name = "device.json";
302 device.serialize_to_file(folder, file_name).unwrap();
303 let path = format!("{}/{}", folder, file_name);
304 assert!(std::fs::metadata(path).is_ok());
305 }
306
307 #[test]
308 fn test_serialize_deserialize_to_file() {
309 let device = Device::create_device(IP, TOKEN, DEVICE_TYPE).unwrap();
310 let folder = std::env::temp_dir();
311 let folder = folder.to_str().unwrap();
312 let file_name = "device.json";
313 device.serialize_to_file(folder, file_name).unwrap();
314 let deserialized_device = Device::deserialize_from_file(folder, file_name).unwrap();
315 assert_eq!(
316 device.serialized_py_object,
317 deserialized_device.serialized_py_object
318 );
319 assert_eq!(
320 device.callable_methods,
321 deserialized_device.callable_methods
322 );
323 }
324}