1use error::TpLinkHs110Error;
3use serde_json::{json, Value};
4use std::{
5 fmt::Display,
6 io::{Read, Write},
7 mem::size_of,
8 net::{self, SocketAddr},
9 ops::Not,
10 time::Duration,
11};
12
13pub mod error;
14
15const NET_BUFFER_SIZE: usize = 8192;
16
17#[derive(Debug)]
19pub struct HS110 {
20 socket_addr: SocketAddr,
22
23 timeout: Option<Duration>,
25}
26
27impl HS110 {
28 pub fn new(addr: &str) -> Result<Self, TpLinkHs110Error> {
30 let socket_addr = match addr.find(':') {
31 Some(_) => addr.parse(),
32 None => (addr.to_string() + ":9999").parse(),
33 }?;
34
35 Ok(Self {
36 socket_addr,
37 timeout: None,
38 })
39 }
40
41 pub fn with_timeout(mut self, duration: Duration) -> Self {
43 self.timeout = Some(duration);
44 self
45 }
46
47 fn encrypt<S>(payload: S) -> Vec<u8>
51 where
52 S: AsRef<str>,
53 {
54 let mut key = 171;
55
56 (payload.as_ref().len() as u32)
57 .to_be_bytes()
58 .into_iter()
59 .chain(payload.as_ref().as_bytes().iter().map(|v| {
60 key ^= v;
61 key
62 }))
63 .collect()
64 }
65
66 fn decrypt(payload: &[u8]) -> Result<String, TpLinkHs110Error> {
68 const HEADER_LEN: usize = size_of::<u32>();
69 if payload.len() < HEADER_LEN {
70 Err(TpLinkHs110Error::ShortEncryptedResponse(payload.len()))?
71 }
72
73 let payload_len_from_header = u32::from_be_bytes(payload[..HEADER_LEN].try_into()?);
74 let payload_len_actual = payload.len() - HEADER_LEN;
75 if payload_len_actual != payload_len_from_header as usize {
76 Err(TpLinkHs110Error::EncryptedPayloadLengthMismatch {
77 payload_len_actual,
78 payload_len_from_header,
79 })?;
80 }
81
82 let mut key = 171;
83 let decrypted: String = payload[HEADER_LEN..]
84 .iter()
85 .map(|byte| {
86 let plain_char = (key ^ byte) as char;
87 key = *byte;
88 plain_char
89 })
90 .collect();
91
92 Ok(decrypted)
93 }
94
95 fn request<S>(&self, request: S) -> Result<String, TpLinkHs110Error>
98 where
99 S: AsRef<str>,
100 {
101 let mut stream = match self.timeout {
102 None => net::TcpStream::connect(self.socket_addr)?,
103 Some(duration) => {
104 let stream = net::TcpStream::connect_timeout(&self.socket_addr, duration)?;
105 stream.set_read_timeout(self.timeout)?;
106 stream.set_write_timeout(self.timeout)?;
107 stream
108 }
109 };
110
111 stream.write_all(&Self::encrypt(request))?;
112 stream.flush()?;
113
114 let mut received = vec![];
115 let mut rx_buf = [0u8; NET_BUFFER_SIZE];
116 loop {
117 let nread = stream.read(&mut rx_buf)?;
118 received.extend_from_slice(&rx_buf[..nread]);
119 if nread < NET_BUFFER_SIZE {
120 break;
121 }
122 }
123
124 Self::decrypt(&received)
125 }
126
127 pub fn info(&self) -> Result<Value, TpLinkHs110Error> {
161 Ok(serde_json::from_str::<Value>(&self.request(
162 json!({"system": {"get_sysinfo": {}}}).to_string(),
163 )?)?)
164 }
165
166 fn info_field_value(&self, field: &'static str) -> Result<Value, TpLinkHs110Error> {
169 self.info()?
170 .extract_hierarchical(&["system", "get_sysinfo", field])
171 }
172
173 pub fn led_state(&self) -> Result<LedState, TpLinkHs110Error> {
175 Ok((self
176 .info_field_value("led_off")?
177 .as_u64()
178 .ok_or(TpLinkHs110Error::UnexpectedValueRepresentation)?
179 == 0)
180 .into())
181 }
182
183 pub fn set_led_state(&self, led_state: LedState) -> Result<(), TpLinkHs110Error> {
185 match serde_json::from_str::<Value>(
186 &self.request(
187 json!({"system": {"set_led_off": {"off": (led_state == LedState::Off) as u8 }}})
188 .to_string(),
189 )?,
190 )?
191 .extract_hierarchical(&["system", "set_led_off", "err_code"])?
192 .as_i64()
193 .ok_or(TpLinkHs110Error::UnexpectedValueRepresentation)?
194 {
195 0 => Ok(()),
196 err_code => Err(TpLinkHs110Error::SmartplugErrCode(err_code)),
197 }
198 }
199
200 pub fn hostname(&self) -> Result<String, TpLinkHs110Error> {
203 Ok(self
204 .info_field_value("alias")?
205 .as_str()
206 .ok_or(TpLinkHs110Error::UnexpectedValueRepresentation)?
207 .to_string())
208 }
209
210 pub fn hw_version(&self) -> Result<HwVersion, TpLinkHs110Error> {
212 match self
213 .info_field_value("hw_ver")?
214 .as_str()
215 .ok_or(TpLinkHs110Error::UnexpectedValueRepresentation)?
216 {
217 "1.0" => Ok(HwVersion::Version1),
218 "2.0" => Ok(HwVersion::Version2),
219 other => Ok(HwVersion::Unsupported(other.into())),
220 }
221 }
222
223 pub fn power_state(&self) -> Result<PowerState, TpLinkHs110Error> {
226 Ok((self
227 .info_field_value("relay_state")?
228 .as_u64()
229 .ok_or(TpLinkHs110Error::UnexpectedValueRepresentation)?
230 == 1)
231 .into())
232 }
233
234 pub fn set_power_state(&self, state: PowerState) -> Result<(), TpLinkHs110Error> {
236 match serde_json::from_str::<Value>(
237 &self.request(
238 json!({"system": {"set_relay_state": {"state": (state == PowerState::On) as u8 }}})
239 .to_string(),
240 )?,
241 )?
242 .extract_hierarchical(&["system", "set_relay_state", "err_code"])?
243 .as_i64()
244 .ok_or(TpLinkHs110Error::UnexpectedValueRepresentation)?
245 {
246 0 => Ok(()),
247 err_code => Err(TpLinkHs110Error::SmartplugErrCode(err_code)),
248 }
249 }
250
251 pub fn cloudinfo(&self) -> Result<Value, TpLinkHs110Error> {
270 serde_json::from_str::<Value>(
271 &self.request(json!({"cnCloud": {"get_info": {}}}).to_string())?,
272 )?
273 .extract_hierarchical(&["cnCloud", "get_info"])
274 }
275
276 pub fn ap_list(&self, refresh: bool) -> Result<Value, TpLinkHs110Error> {
303 serde_json::from_str::<Value>(
304 &self.request(
305 json!({"netif": {"get_scaninfo": {"refresh": refresh as u8}}}).to_string(),
306 )?,
307 )?
308 .extract_hierarchical(&["netif", "get_scaninfo", "ap_list"])
309 }
310
311 pub fn emeter(&self) -> Result<Value, TpLinkHs110Error> {
329 let mut emeter = serde_json::from_str::<Value>(
330 &self.request(json!({"emeter":{"get_realtime":{}}}).to_string())?,
331 )?
332 .extract_hierarchical(&["emeter", "get_realtime"])?;
333
334 #[rustfmt::skip]
341 [
342 ("voltage_mv", "voltage", 0.001f64),
343 ("current_ma", "current", 0.001f64),
344 ("power_mw", "power", 0.001f64),
345 ("total_wh", "total", 0.001f64),
346 ("voltage", "voltage_mv", 1000f64),
347 ("current", "current_ma", 1000f64),
348 ("power", "power_mw", 1000f64),
349 ("total", "total_wh", 1000f64),
350 ]
351 .iter()
352 .for_each(|(from, to, multiplier)| {
353 if let Some(from) = emeter.get(from) {
354 if emeter.get(to).is_none() {
355 emeter[to] = Value::from(from.as_f64().unwrap_or(0f64) * multiplier);
356 }
357 }
358 });
359
360 Ok(emeter)
361 }
362
363 pub fn reboot(&self, delay: Option<u32>) -> Result<(), TpLinkHs110Error> {
365 match serde_json::from_str::<Value>(
366 &self.request(
367 json!({"system": {"reboot": {"delay": delay.unwrap_or(0) }}}).to_string(),
368 )?,
369 )?
370 .extract_hierarchical(&["system", "reboot", "err_code"])?
371 .as_i64()
372 .ok_or(TpLinkHs110Error::UnexpectedValueRepresentation)?
373 {
374 0 => Ok(()),
375 err_code => Err(TpLinkHs110Error::SmartplugErrCode(err_code)),
376 }
377 }
378
379 pub fn factory_reset(&self, delay: Option<u32>) -> Result<(), TpLinkHs110Error> {
381 match serde_json::from_str::<Value>(
382 &self.request(
383 json!({"system": {"reset": {"delay": delay.unwrap_or(0) }}}).to_string(),
384 )?,
385 )?
386 .extract_hierarchical(&["system", "reset", "err_code"])?
387 .as_i64()
388 .ok_or(TpLinkHs110Error::UnexpectedValueRepresentation)?
389 {
390 0 => Ok(()),
391 err_code => Err(TpLinkHs110Error::SmartplugErrCode(err_code)),
392 }
393 }
394}
395
396trait ExtractHierarchical {
397 fn extract_hierarchical(&self, path: &[&'static str]) -> Result<Value, TpLinkHs110Error>;
398}
399
400impl ExtractHierarchical for Value {
401 fn extract_hierarchical(&self, path: &[&'static str]) -> Result<Value, TpLinkHs110Error> {
404 let mut current_object = self;
405 for key in path {
406 current_object =
407 current_object
408 .get(key)
409 .ok_or_else(|| TpLinkHs110Error::KeyIsNotAvailable {
410 response: self.clone(),
411 key,
412 })?;
413 }
414
415 Ok(current_object.clone())
416 }
417}
418
419#[derive(Debug)]
421pub enum HwVersion {
422 Version1,
423 Version2,
424 Unsupported(String),
425}
426
427#[derive(Debug, Clone, Copy, PartialEq)]
429pub enum PowerState {
430 On,
432
433 Off,
435}
436
437impl Display for PowerState {
438 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
439 write!(
440 f,
441 "{}",
442 match self {
443 PowerState::On => "ON",
444 PowerState::Off => "OFF",
445 }
446 )
447 }
448}
449
450impl Not for PowerState {
451 type Output = PowerState;
452
453 fn not(self) -> Self::Output {
454 match self {
455 PowerState::On => PowerState::Off,
456 PowerState::Off => PowerState::On,
457 }
458 }
459}
460
461impl From<PowerState> for bool {
462 fn from(value: PowerState) -> Self {
463 match value {
464 PowerState::On => true,
465 PowerState::Off => false,
466 }
467 }
468}
469
470impl From<bool> for PowerState {
471 fn from(value: bool) -> Self {
472 match value {
473 true => Self::On,
474 false => Self::Off,
475 }
476 }
477}
478
479#[derive(Debug, Clone, Copy, PartialEq)]
481pub enum LedState {
482 On,
484
485 Off,
487}
488
489impl Display for LedState {
490 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
491 write!(
492 f,
493 "{}",
494 match self {
495 LedState::On => "ON",
496 LedState::Off => "OFF",
497 }
498 )
499 }
500}
501
502impl Not for LedState {
503 type Output = LedState;
504
505 fn not(self) -> Self::Output {
506 match self {
507 LedState::On => LedState::Off,
508 LedState::Off => LedState::On,
509 }
510 }
511}
512
513impl From<LedState> for bool {
514 fn from(value: LedState) -> Self {
515 match value {
516 LedState::On => true,
517 LedState::Off => false,
518 }
519 }
520}
521
522impl From<bool> for LedState {
523 fn from(value: bool) -> Self {
524 match value {
525 true => Self::On,
526 false => Self::Off,
527 }
528 }
529}
530
531#[cfg(test)]
532mod tests {
533 use crate::*;
534 use once_cell::sync::Lazy;
535 use serial_test::serial;
536
537 static TEST_TARGET_ADDR: Lazy<String> =
538 Lazy::new(|| std::env::var("TEST_TARGET_ADDR").expect("TEST_TARGET_ADDR env variable"));
539
540 #[test]
541 #[serial]
542 fn hostname() {
543 let smartplug = HS110::new(&*TEST_TARGET_ADDR)
544 .unwrap()
545 .with_timeout(Duration::from_secs(3));
546 assert!(smartplug.hostname().is_ok());
547
548 let smartplug = HS110::new(&*TEST_TARGET_ADDR).unwrap();
549 assert!(smartplug.hostname().is_ok());
550
551 assert!(matches!(
552 smartplug.hw_version(),
553 Ok(HwVersion::Version1) | Ok(HwVersion::Version2)
554 ));
555 }
556
557 #[test]
558 fn switch_led_on_off() {
559 let smartplug = HS110::new(&*TEST_TARGET_ADDR).unwrap();
560
561 let original_state = smartplug.led_state().expect("failed to obtain LED state");
562
563 assert!(smartplug.set_led_state(!original_state).is_ok());
564 assert_eq!(
565 smartplug.led_state().expect("failed to obtain LED state"),
566 !original_state
567 );
568
569 assert!(smartplug.set_led_state(original_state).is_ok());
570 assert_eq!(
571 smartplug.led_state().expect("failed to obtain LED state"),
572 original_state
573 );
574 }
575
576 #[test]
577 #[serial]
578 #[ignore = "power-cycles devices connected to the plug"]
579 fn switch_power_on_off() {
580 let smartplug = HS110::new(&*TEST_TARGET_ADDR).unwrap();
581
582 let original_state = smartplug
583 .power_state()
584 .expect("failed to obtain smartplug power state");
585
586 assert!(smartplug.set_power_state(!original_state).is_ok());
587 assert_eq!(
588 smartplug
589 .power_state()
590 .expect("failed to obtain smartplug power state"),
591 !original_state
592 );
593
594 assert!(smartplug.set_power_state(original_state).is_ok());
595 assert_eq!(
596 smartplug
597 .power_state()
598 .expect("failed to obtain smartplug power state"),
599 original_state
600 );
601 }
602
603 #[test]
604 fn get_cloudinfo() {
605 assert!(HS110::new(&*TEST_TARGET_ADDR).unwrap().cloudinfo().is_ok());
606 }
607
608 #[test]
609 #[serial]
610 fn access_points_list_and_scan() {
611 let smartplug = HS110::new(&*TEST_TARGET_ADDR).unwrap();
612
613 smartplug
614 .ap_list(false)
615 .expect("failed to obtain AP list")
616 .as_array()
617 .expect("json array is expected");
618
619 assert!(
620 !smartplug
621 .ap_list(true)
622 .expect("failed to obtain AP list")
623 .as_array()
624 .expect("json array is expected")
625 .is_empty(),
626 "list of access points is not expected to be empty"
627 );
628 }
629
630 #[test]
631 #[serial]
632 #[ignore = "power-cycles devices connected to the plug"]
633 fn reboot() {
634 let hs110 = HS110::new(&*TEST_TARGET_ADDR).unwrap();
635 assert!(hs110.reboot(None).is_ok());
636
637 let hs110 = hs110.with_timeout(Duration::from_secs(1));
638 assert!(
639 hs110.reboot(Some(1)).is_err(),
640 "device is expected to be unreachable right after reboot command"
641 );
642
643 let hs110 = hs110.with_timeout(Duration::from_secs(10));
645 for _ in 0..20 {
646 if hs110.hostname().is_ok() {
647 return;
648 }
649 }
650 panic!("device didn't back online after reboot");
651 }
652}