1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
extern crate byteorder;

#[macro_use]
extern crate serde_derive;
extern crate serde_json;
use std::io::prelude::*;
use std::net::TcpStream;
use byteorder::{BigEndian, WriteBytesExt};
use std::time::Duration;
use std::str;

pub mod types;
pub mod error;

use types::*;
use error::*;

pub struct SmartPlug {
    ip: &'static str,
}

impl SmartPlug {
    pub fn new(ip: &'static str) -> SmartPlug {
        SmartPlug { ip: ip }
    }

    // Wakes up the device
    pub fn on(&self) -> Result<PlugInfo, Error> {
        let json = "{\"system\":{\"set_relay_state\":{\"state\":1}}}";
        self.submit_to_device(json)
    }

    // Turns off the device
    pub fn off(&self) -> Result<PlugInfo, Error> {
        let json = "{\"system\":{\"set_relay_state\":{\"state\":0}}}";
        self.submit_to_device(json)
    }

    // Gather system wide info such as model of the device, etc.
    pub fn sysinfo(&self) -> Result<PlugInfo, Error> {
        let json = "{\"system\":{\"get_sysinfo\":{}}}";
        self.submit_to_device(json)
    }

    // Gather system information as well as watt meter information
    pub fn meterinfo(&self) -> Result<PlugInfo, Error> {
        let json = "{\"system\":{\"get_sysinfo\":{}}, \"emeter\":{\"get_realtime\":{},\"get_vgain_igain\":{}}}";
        self.submit_to_device(json)
    }

    // Returns system information as well as daily statistics of power usage
    pub fn dailystats(&self, month: i32, year: i32) -> Result<PlugInfo, Error> {
        let json = format!(
            "{{\"emeter\":{{\"get_daystat\":{{\"month\":{},\"year\":{}}}}}}}",
            month,
            year
        );
        self.submit_to_device(&json)
    }

    fn submit_to_device(&self, msg: &str) -> Result<PlugInfo, Error> {
        let msg = try!(encrypt(msg));
        let mut resp = try!(send(self.ip, &msg));
        let data = decrypt(&mut resp.split_off(4));

        // deserialize json
        let resp = try!(serde_json::from_str(&data));

        Ok(resp)
    }
}

// Prepare and encrypt message to send to the device
// see: https://www.softscheck.com/en/reverse-engineering-tp-link-hs110/
fn encrypt(plain: &str) -> Result<Vec<u8>, Error> {
    let len = plain.len();
    let msgbytes = plain.as_bytes();
    let mut cipher = vec![];
    try!(cipher.write_u32::<BigEndian>(len as u32));

    let mut key = 0xAB;
    let mut payload: Vec<u8> = Vec::with_capacity(len);

    for i in 0..len {
        payload.push(msgbytes[i] ^ key);
        key = payload[i];
    }

    for i in &payload {
        try!(cipher.write_u8(*i));
    }

    Ok(cipher)
}

// Decrypt received string
// see: https://www.softscheck.com/en/reverse-engineering-tp-link-hs110/
fn decrypt(cipher: &mut [u8]) -> String {
    let len = cipher.len();

    let mut key = 0xAB;
    let mut next: u8;

    for i in 0..len {
        next = cipher[i];
        cipher[i] ^= key;
        key = next;
    }

    String::from_utf8_lossy(cipher).into_owned()
}

// Sends a message to the device and awaits a response synchronously
fn send(ip: &str, payload: &[u8]) -> Result<Vec<u8>, Error> {
    let mut stream = try!(TcpStream::connect(ip));

    try!(stream.set_read_timeout(Some(Duration::new(5, 0))));
    try!(stream.write_all(payload));

    let mut resp = vec![];
    try!(stream.read_to_end(&mut resp));

    Ok(resp)
}

#[cfg(test)]
mod tests {
    use encrypt;
    use decrypt;

    #[test]
    fn encrypt_decrypt() {
        let json = "{\"system\":{\"get_sysinfo\":{}}}";

        let mut data = encrypt(json);
        let resp = decrypt(&mut data.split_off(4));

        assert_eq!(json, resp);
    }
}