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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
use crate::lightstructs::*;
use dotenv;
use reqwest::Client;
use serde_json::value::Value;
use std::collections::BTreeMap;
use std::env;
use std::error::Error;
use std::fmt;
use std::fs::File;
use std::io::prelude::*;
use std::thread::sleep;
use std::time::Duration;

type Lights = BTreeMap<u8, Light>;

/// The bridge struct represents a HUE bridge.
/// The constructor for this struct `link`, tries to
/// detect the lights and is able to send new state to either
/// a single light or all of the ones detected.
///
/// If you already have an application key and IP use the `link` method to create a bridge. However you
/// must make sure tha the HUE_IP and HUE_KEY environment variables are set in your environment
/// or the `.huemanity` file in the home directory has these variables set.
///
/// If you don't have the key registered yet, the link function will guide you through the
/// process to register the key and save it to the `.huemanity` file that will be loaded by the CLI
/// everytime.
#[derive(Debug)]
pub struct Bridge {
    ip: String,
    key: String,
    client: Client,
    base_url: String,
    pub light_ids: Vec<u8>,
    pub n_lights: u8,
    pub lights: Option<Lights>,
}

impl Bridge {
    /// Discovers if a `HUE_IP` and `HUE_KEY` are available in the environment
    fn discover(filename: &str) -> Result<(String, String), Box<dyn Error>> {
        dotenv::from_filename(filename)?;
        let ip = env::var("HUE_IP")?;
        let key = env::var("HUE_KEY")?;
        Ok((ip, key))
    }

    /// Register the Bridge and save credentials to `~/.huemanity` file
    /// Can be used as a standalone function to get a key registered
    /// if you know the IP of your Bridge, but generally is a helper for
    /// the `link` method.
    pub fn register(configpath: &str) -> Result<(String, String), Box<dyn Error>> {
        // TODO: currently uses file writting rather than some more clever serialisation and checking
        // TODO: could also take an optional setting string or config path ?
        // TODO: write a serialisation (serde) so one can load the bridge from config
        let _client = Client::new();
        let mut ip = String::new();
        let mut name = String::new();

        // Get user IP input and name for the app
        println!("NOTE! Registration will create the `~/.huemanity` containing IP and KEY info");
        println!("Enter the IP of your HUE bridge:");
        std::io::stdin().read_line(&mut ip)?;
        ip = ip.trim().to_string();

        println!("Enter the desired app name:");
        std::io::stdin().read_line(&mut name)?;
        name = name.trim().to_string();

        // only use json! here because its a one of and writing serialisation for it is pointless
        let body = serde_json::json!({ "devicetype": format!("{}", name) });
        let ping_it = || -> Value {
            _client
                .post(&format!("http://{}/api", ip))
                .json(&body)
                .send()
                .unwrap()
                .json()
                .unwrap()
        };

        let mut response = ping_it();

        loop {
            if response[0]["error"]["type"] == 101 {
                println!("Please press the hub button!");
                sleep(Duration::from_secs(5));
                response = ping_it();
            } else {
                break;
            }
        }

        let key = &response[0]["success"]["username"];

        let mut file = File::create(configpath)?;
        file.write_all(format!("HUE_IP=\"{}\"\nHUE_KEY={}", ip, key).as_ref())?;
        println!(".env File successfully saved!");

        // TODO: hacky replace
        Ok((ip, key.to_string().replace("\"", "")))
    }

    /// Struct constructor that sets up the required interactions
    /// and also gets us the lights that it can find on the system
    ///
    /// If you have `HUE_IP` and `HUE_KEY` in your environment this will
    /// just proceed as normal linking to the bridge. If you don't have these
    /// variables in the environment, it will try to guide you throught a registration
    /// process. In that case you will still need to know the IP of your Bridge.
    pub fn link() -> Self {
        let mut filename = dirs::home_dir().unwrap();
        filename.push(".huemanity");
        let path = filename.to_str().unwrap();

        let client = Client::new();

        // discovery of IP and registration logic
        let (ip, key) = match Self::discover(path) {
            Ok(tupl) => tupl,
            _ => {
                println!("Unable to find required `HUE_KEY` and `HUE_IP` in environment!");
                let result = match Self::register(path) {
                    Ok(tupl) => {
                        println!("Registration successful");
                        tupl
                    }
                    Err(e) => panic!("Could not register due to: {}", e),
                };
                result
            }
        };

        let base_url = format!("http://{}/api/{}/", ip, key);
        let mut bridge = Bridge {
            ip,
            key,
            client,
            base_url,
            light_ids: Vec::new(),
            n_lights: 0,
            lights: None,
        };

        // collect the lights into the bridge
        match bridge.collect_lights() {
            Ok(_) => println!("Collected lights sucessfully!"),
            Err(e) => println!("Could not collect lights: {}", e),
        }

        println!("Connected to:\n{}", bridge);
        println!("Found {} lights", bridge.n_lights);
        bridge
    }

    /// Sends the a request with set parameters to the HUE API endpoint
    /// This is a lower level function used primarily to send state.
    /// For more useful functions to look at: `Bridge.state` , `Bridge.state_all`
    fn send(
        &self,
        endpoint: &str,
        req_type: RequestType,
        params: Option<&SendableState>,
    ) -> Result<reqwest::Response, Box<dyn std::error::Error>> {
        // TODO: make it so it takes the state, and fills in the values from the same light
        let target = format!("{}{}", self.base_url, endpoint);
        let response = match req_type {
            RequestType::Post => self.client.post(&target).json(&params).send()?,
            RequestType::Get => self.client.get(&target).send()?,
            RequestType::Put => self.client.put(&target).json(&params).send()?,
        };
        Ok(response)
    }

    /// Given a light and a required state, send this state to the light.
    pub fn state(
        &self,
        light: u8,
        state: &SendableState,
    ) -> Result<(), Box<dyn std::error::Error>> {
        // TODO: Implement a threadpool solution where the pool is owned by the bridge and you
        // send light commands through that.
        self.send(
            &format!("lights/{}/state", light),
            RequestType::Put,
            Some(state),
        )?;
        Ok(())
    }

    /// Given a state send it to all lights found on bridge.
    /// At the moment it is done in a loop. So the lights don't get the
    /// signal sent concurrently
    pub fn state_all(&self, state: &SendableState) -> Result<(), Box<dyn std::error::Error>> {
        for light in self.light_ids.iter() {
            self.state(*light, state)?;
        }
        Ok(())
    }

    /// Collect all found light ids
    /// This method updates the following attributes of the bridge:
    /// - light_ids
    /// - n_lights
    /// - lights
    pub fn collect_lights(&mut self) -> Result<(), Box<dyn std::error::Error>> {
        // get the lights state
        let lights: Lights = self.send("lights", RequestType::Get, None)?.json()?;

        // update the values with the new ones
        self.light_ids = lights.keys().cloned().map(|integer| integer).collect();
        self.lights = Some(lights);
        self.n_lights = self.light_ids.len() as u8;

        Ok(())
    }

    /// This is a simple method to show the lights in the terminal
    pub fn light_info(&self) {
        // TODO: make it sorted
        println!("Lights available on your bridge:");

        let lights = self.lights.as_ref().unwrap();
        for (id, light) in lights.iter() {
            println!("{}:\n{:?}", id, light);
        }
    }
}

impl fmt::Display for Bridge {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "bridge: {}\nlights: {:?}", self.ip, self.light_ids)
    }
}