├── assets └── WeeApp.app │ └── Contents │ ├── MacOs │ └── put_executeable_here.txt │ ├── Resources │ └── icon.icns │ └── Info.plist ├── resources.rc ├── addicon.bat ├── examples ├── images │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ ├── icon.xcf │ ├── titlebar.png │ ├── titlebar.xcf │ ├── progress.svg │ ├── clear-hover.svg │ ├── refresh-hover.svg │ ├── refresh.svg │ ├── refresh-press.svg │ ├── clear.svg │ └── clear-press.svg ├── cli.rs └── weeapp.rs ├── .travis.yml ├── .gitignore ├── src ├── lib.rs ├── error.rs ├── cache.rs ├── xml.rs ├── rpc.rs ├── device.rs └── weectrl.rs ├── LICENSE-MIT ├── Cargo.toml ├── README.md ├── LICENSE-APACHE └── Cargo.lock /assets/WeeApp.app/Contents/MacOs/put_executeable_here.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources.rc: -------------------------------------------------------------------------------- 1 | ApplicationIcon ICON "examples\images\icon.ico" -------------------------------------------------------------------------------- /addicon.bat: -------------------------------------------------------------------------------- 1 | rcedit target\release\examples\weeapp.exe --set-icon examples\images\icon.ico 2 | -------------------------------------------------------------------------------- /examples/images/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hyperchaotic/weectrl/HEAD/examples/images/icon.icns -------------------------------------------------------------------------------- /examples/images/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hyperchaotic/weectrl/HEAD/examples/images/icon.ico -------------------------------------------------------------------------------- /examples/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hyperchaotic/weectrl/HEAD/examples/images/icon.png -------------------------------------------------------------------------------- /examples/images/icon.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hyperchaotic/weectrl/HEAD/examples/images/icon.xcf -------------------------------------------------------------------------------- /examples/images/titlebar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hyperchaotic/weectrl/HEAD/examples/images/titlebar.png -------------------------------------------------------------------------------- /examples/images/titlebar.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hyperchaotic/weectrl/HEAD/examples/images/titlebar.xcf -------------------------------------------------------------------------------- /assets/WeeApp.app/Contents/Resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hyperchaotic/weectrl/HEAD/assets/WeeApp.app/Contents/Resources/icon.icns -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | 3 | rust: 4 | - nightly 5 | 6 | os: 7 | - linux 8 | 9 | script: 10 | - cargo build --verbose 11 | - cargo build --verbose --example weeapp 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | 8 | **/.DS_Store 9 | *.res 10 | *.log 11 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A library for finding and managing Belkin WeMo switches and devices, or compatible devices. 2 | //! 3 | //! WeMo devices use uPnP and HTTP/SOAP for network anouncements and control. The `WeeController` 4 | //! encapsulates this and offers a simpler API for discovery and management. 5 | //! Discovery can be synchronous or asynchronous and a list of known devices is cached on disk 6 | //! for quick startup. 7 | 8 | extern crate reqwest; 9 | 10 | extern crate serde; 11 | extern crate serde_xml_rs; 12 | 13 | extern crate serde_derive; 14 | 15 | extern crate url; 16 | 17 | pub mod weectrl; 18 | pub use weectrl::{ 19 | DeviceInfo, DiscoveryMode, Icon, Model, State, StateNotification, WeeController, 20 | }; 21 | 22 | pub mod error; 23 | 24 | mod cache; 25 | mod device; 26 | mod rpc; 27 | mod xml; 28 | -------------------------------------------------------------------------------- /assets/WeeApp.app/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleGetInfoString 6 | WeeApp 7 | CFBundleExecutable 8 | weeapp 9 | CFBundleIdentifier 10 | com.hyperchaotic.weeapp 11 | CFBundleName 12 | WeeApp 13 | CFBundleIconFile 14 | icon.icns 15 | CFBundleShortVersionString 16 | 0.9.6 17 | CFBundleInfoDictionaryVersion 18 | 6.0 19 | CFBundlePackageType 20 | APPL 21 | IFMajorVersion 22 | 0 23 | IFMinorVersion 24 | 1 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Klaus Nielsen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "weectrl" 3 | version = "2.0.2" 4 | authors = ["Klaus Nielsen "] 5 | license = "MIT/Apache-2.0" 6 | readme = "README.md" 7 | keywords = ["WeMo"] 8 | repository = "https://github.com/Hyperchaotic/weectrl" 9 | homepage = "https://github.com/Hyperchaotic/weectrl" 10 | description = "A library and application for controlling WeMo switches." 11 | categories = ["Network Programming"] 12 | edition = "2021" 13 | 14 | 15 | [dependencies] 16 | tracing = "0.1" 17 | tracing-subscriber = { version = "0.3", features = ["local-time"] } 18 | directories = "5" 19 | futures = "0.3" 20 | mime = "0.3.16" 21 | serde-xml-rs = "0.6" 22 | serde_json = "1.0" 23 | serde_derive = "1.0" 24 | serde = { version = "1.0", features = ["derive"] } 25 | url = "2" 26 | reqwest = { version = "0.11", features = ["blocking"] } 27 | hyper = { version = "0.14", features = ["full"] } 28 | tokio = { version = "1", features = ["full"] } 29 | 30 | [profile.release] 31 | strip = true 32 | opt-level = "s" 33 | lto = true 34 | codegen-units = 1 35 | panic = "abort" 36 | 37 | [dev-dependencies] 38 | advmac = "1" 39 | fltk = "^1.5" 40 | fltk-theme = "0.7" 41 | 42 | [lib] 43 | name = "weectrl" 44 | path = "src/lib.rs" 45 | -------------------------------------------------------------------------------- /examples/images/progress.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 35 | 40 | 41 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use reqwest::header::InvalidHeaderValue; 2 | use reqwest::Error as HttpError; 3 | use reqwest::StatusCode; 4 | use serde_xml_rs::Error as SerdeError; 5 | use std::io; 6 | use std::num::ParseIntError; 7 | use url::ParseError; 8 | 9 | /// If a semaphore fails, panic immediately with this message. 10 | pub const FATAL_LOCK: &str = "FATAL Error, Lock failed!"; 11 | 12 | #[derive(Debug)] 13 | pub enum Error { 14 | InvalidState, 15 | NoResponse, 16 | UnknownDevice, 17 | UnsupportedDevice, 18 | DeviceAlreadyRegistered, 19 | DeviceError, 20 | ServiceAlreadyRunning, 21 | ServiceNotRunning, 22 | NotSubscribed, 23 | AutoResubscribeRunning, 24 | TimeoutTooShort, 25 | SoapResponseError(SerdeError), 26 | InvalidResponse(StatusCode), 27 | NetworkError(HttpError), 28 | IoError(io::Error), 29 | UrlError(ParseError), 30 | ParseError(ParseIntError), 31 | HttpHeaderError, 32 | } 33 | 34 | impl From for Error { 35 | fn from(_err: InvalidHeaderValue) -> Self { 36 | Self::HttpHeaderError 37 | } 38 | } 39 | 40 | impl From for Error { 41 | fn from(err: ParseIntError) -> Self { 42 | Self::ParseError(err) 43 | } 44 | } 45 | 46 | impl From for Error { 47 | fn from(err: SerdeError) -> Self { 48 | Self::SoapResponseError(err) 49 | } 50 | } 51 | 52 | impl From for Error { 53 | fn from(err: ParseError) -> Self { 54 | Self::UrlError(err) 55 | } 56 | } 57 | 58 | impl From for Error { 59 | fn from(err: io::Error) -> Self { 60 | Self::IoError(err) 61 | } 62 | } 63 | 64 | impl From for Error { 65 | fn from(err: HttpError) -> Self { 66 | Self::NetworkError(err) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /examples/cli.rs: -------------------------------------------------------------------------------- 1 | use futures::prelude::*; 2 | use std::time::Duration; 3 | use tokio::io::{self, AsyncBufReadExt}; 4 | use weectrl::{DiscoveryMode, WeeController}; 5 | 6 | #[tokio::main] 7 | async fn main() { 8 | // Uncomment below to see tracing from the library 9 | //use tracing_subscriber::fmt::time; 10 | //tracing_subscriber::fmt() 11 | // .with_timer(time::LocalTime::rfc_3339()) 12 | // .init(); 13 | 14 | println!("Instantiating WeeController.\n"); 15 | let controller = WeeController::new(); 16 | 17 | // Find switches on the network. 18 | let mut discovery_future = controller.discover_future( 19 | DiscoveryMode::CacheAndBroadcast, 20 | true, 21 | Duration::from_secs(5), 22 | ); 23 | 24 | println!("Searching for devices...\n"); 25 | while let Some(d) = discovery_future.next().await { 26 | println!( 27 | " Found device {}, ID: {}, state: {:?}.", 28 | d.friendly_name, d.unique_id, d.state 29 | ); 30 | let res = controller.subscribe( 31 | &d.unique_id, 32 | Duration::from_secs(120), 33 | true, 34 | Duration::from_secs(5), 35 | ); 36 | println!( 37 | " - Subscribed {} - {:?} seconds to resubscribe.\n", 38 | d.friendly_name, 39 | res.unwrap() 40 | ); 41 | } 42 | 43 | // Create notification future. 44 | let notifier_future = controller.notify_future(); 45 | let notifier = notifier_future.for_each(|o| { 46 | if let Ok(e) = controller.get_info(&o.unique_id) { 47 | println!( 48 | " Device {} sent state update {:?}", 49 | e.friendly_name, o.state 50 | ); 51 | } 52 | future::ready(()) 53 | }); 54 | 55 | // Input buffer, for keyboard cancel on Enter. 56 | let mut buf = String::new(); 57 | let mut reader = io::BufReader::new(io::stdin()); 58 | let quit = reader.read_line(&mut buf); 59 | 60 | println!("\n\nListening for notifications from switches."); 61 | println!("Press Enter to quit.\n"); 62 | tokio::select! { 63 | _ = quit => (), 64 | 65 | _ = notifier => (), 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /examples/images/clear-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 35 | 39 | 43 | 47 | 51 | 55 | 59 | 60 | -------------------------------------------------------------------------------- /src/cache.rs: -------------------------------------------------------------------------------- 1 | extern crate directories; 2 | use directories::ProjectDirs; 3 | 4 | use tracing::info; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use std::fs::File; 9 | use std::io::prelude::*; 10 | 11 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 12 | pub struct DeviceAddress { 13 | pub location: String, 14 | pub mac_address: String, 15 | } 16 | 17 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 18 | struct CacheData { 19 | pub cache: Vec, 20 | } 21 | 22 | const CACHE_FILE: &str = "IPCache.dat"; 23 | 24 | /// Disk cache of IP addresses of WeMo devices 25 | /// WeMo doesn't always answer broadcast upnp, this cache allows 26 | /// for the persistent storage of known devices for unicast discovery 27 | pub struct DiskCache { 28 | cache_file: Option, 29 | } 30 | 31 | impl DiskCache { 32 | pub fn new() -> Self { 33 | let mut file_path: Option = None; 34 | 35 | if let Some(proj_dirs) = ProjectDirs::from("", "", "WeeCtrl") { 36 | let mut path = proj_dirs.config_dir().to_path_buf(); 37 | path.push(CACHE_FILE); 38 | file_path = Some(path); 39 | info!("Cache file: {:#?}", file_path); 40 | } 41 | Self { 42 | cache_file: file_path, 43 | } 44 | } 45 | 46 | /// Write data to cache, errors ignored as everything will still work 47 | /// without a cache, just slower. 48 | pub fn write(&self, addresses: Vec) { 49 | if let Some(ref fpath) = self.cache_file { 50 | let data = CacheData { cache: addresses }; 51 | if let Some(prefix) = fpath.parent() { 52 | let _ignore = std::fs::create_dir_all(prefix); 53 | 54 | if let Ok(serialized) = serde_json::to_string(&data) { 55 | if let Ok(mut buffer) = File::create(fpath) { 56 | let _ignore = buffer.write_all(&serialized.into_bytes()); 57 | } 58 | } 59 | } 60 | } 61 | } 62 | 63 | pub fn read(&self) -> Option> { 64 | if let Some(ref fpath) = self.cache_file { 65 | if let Ok(mut file) = File::open(fpath) { 66 | let mut s = String::new(); 67 | let _ignore = file.read_to_string(&mut s); 68 | let cachedata: Option = serde_json::from_str(&s).ok(); 69 | if let Some(data) = cachedata { 70 | return Some(data.cache); 71 | } 72 | } 73 | } 74 | None 75 | } 76 | 77 | pub fn clear(&self) { 78 | if let Some(ref fpath) = self.cache_file { 79 | let _ignore = std::fs::remove_file(fpath); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /examples/images/refresh-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 35 | 39 | 43 | 46 | 50 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /examples/images/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 35 | 39 | 43 | 46 | 50 | 54 | 55 | 59 | 63 | 64 | -------------------------------------------------------------------------------- /examples/images/refresh-press.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 35 | 39 | 43 | 46 | 50 | 54 | 55 | 59 | 63 | 64 | -------------------------------------------------------------------------------- /src/xml.rs: -------------------------------------------------------------------------------- 1 | pub use crate::error::Error; 2 | 3 | use serde::Deserialize; 4 | 5 | #[derive(Debug, Deserialize, Clone)] 6 | #[serde(rename = "service")] 7 | pub struct Service { 8 | #[serde(rename = "serviceType")] 9 | pub service_type: String, 10 | #[serde(rename = "serviceId")] 11 | pub service_id: String, 12 | #[serde(rename = "controlURL")] 13 | pub control_url: String, 14 | #[serde(rename = "eventSubURL")] 15 | pub event_sub_url: String, 16 | #[serde(rename = "SCPDURL")] 17 | pub scp_url: String, 18 | } 19 | 20 | #[derive(Debug, Deserialize, Clone)] 21 | #[serde(rename = "icon")] 22 | pub struct Icon { 23 | pub mimetype: String, // >jpg 24 | pub width: String, // >100 25 | pub height: String, // >100 26 | pub depth: String, // >100 27 | pub url: String, // >icon.jpg 28 | } 29 | 30 | #[derive(Debug, Deserialize, Clone)] 31 | #[serde(rename = "iconList")] 32 | pub struct IconList { 33 | pub icon: Vec, 34 | } 35 | 36 | #[derive(Debug, Deserialize, Clone)] 37 | #[serde(rename = "serviceList")] 38 | pub struct ServiceList { 39 | pub service: Vec, 40 | } 41 | 42 | #[derive(Debug, Deserialize, Clone)] 43 | #[serde(rename = "device")] 44 | pub struct Device { 45 | #[serde(rename = "deviceType")] 46 | pub device_type: String, // >urn:Belkin:device:lightswitch:1 47 | #[serde(rename = "friendlyName")] 48 | pub friendly_name: String, // >Kitchen light 49 | pub manufacturer: String, // >Belkin International Inc. 50 | #[serde(rename = "manufacturerURL")] 51 | pub manufacturer_url: String, // >http://www.belkin.com 52 | #[serde(rename = "modelDescription")] 53 | pub model_description: String, // >Belkin Plugin Socket 1.0 54 | #[serde(rename = "modelName")] 55 | pub model_name: String, // >LightSwitch 56 | #[serde(rename = "modelNumber")] 57 | pub model_number: String, // >1.0 58 | #[serde(rename = "modelURL")] 59 | pub model_url: String, // >http://www.belkin.com/plugin/ 60 | #[serde(rename = "serialNumber")] 61 | pub serial_number: String, // >221435K13005D9 62 | #[serde(rename = "UDN")] 63 | pub udn: String, // >uuid:Lightswitch-1_0-221435K13005D9 64 | #[serde(rename = "UPC")] 65 | pub upc: String, // >123456789 66 | #[serde(rename = "macAddress")] 67 | pub mac_address: String, // >94103E4830D0 68 | #[serde(rename = "firmwareVersion")] 69 | pub firmware_version: String, // >WeMo_WW_2.00.10937.PVT-OWRT-LS 70 | #[serde(rename = "iconVersion")] 71 | pub icon_version: String, // >0|49153 72 | #[serde(rename = "binaryState")] 73 | pub binary_state: String, // >0 74 | #[serde(rename = "iconList")] 75 | pub icon_list: IconList, 76 | #[serde(rename = "serviceList")] 77 | pub service_list: ServiceList, 78 | #[serde(rename = "presentationURL")] 79 | pub presentation_url: String, 80 | } 81 | 82 | #[derive(Debug, Deserialize, Clone)] 83 | #[serde(rename = "specVersion")] 84 | pub struct SpecVersion { 85 | pub major: String, 86 | pub minor: String, 87 | } 88 | 89 | #[derive(Debug, Deserialize, Clone)] 90 | #[serde(rename = "root")] 91 | pub struct Root { 92 | #[serde(rename = "specVersion")] 93 | pub spec_version: SpecVersion, 94 | pub device: Device, 95 | } 96 | 97 | pub fn parse_services(data: &str) -> Result { 98 | let r: Root = serde_xml_rs::de::from_str(data)?; 99 | Ok(r) 100 | } 101 | 102 | pub fn get_binary_state(data: &str) -> Option { 103 | if data.contains("1") { 104 | return Some(1); 105 | } else if data.contains("0") { 106 | return Some(0); 107 | } 108 | None 109 | } 110 | 111 | pub const SETBINARYSTATEOFF: &str = " 112 | 115 | 116 | \ 117 | 118 | \ 119 | 0 120 | 121 | 122 | "; 123 | 124 | pub const SETBINARYSTATEON: &str = " 125 | 128 | 129 | \ 130 | 131 | \ 132 | 1 133 | 134 | 135 | "; 136 | 137 | pub const GETBINARYSTATE: &str = " 138 | 139 | 142 | 143 | 144 | 1 145 | 146 | 147 | "; 148 | -------------------------------------------------------------------------------- /examples/images/clear.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 35 | 39 | 43 | 47 | 51 | 55 | 59 | 64 | 65 | -------------------------------------------------------------------------------- /examples/images/clear-press.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 35 | 39 | 43 | 47 | 51 | 55 | 59 | 64 | 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # weectrl [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 2 | 3 | A cross platform library and application for controlling Belkin WeMo switches and Sockets. Written in Rust. 4 | 5 | ## WeeApp, the application 6 | 7 | ### Screenshots 8 | ![Screenshot-macOS](https://user-images.githubusercontent.com/19599562/193374117-9890c33d-c4f9-49ca-a1b2-9010daaecfa3.png) 9 | ![info](https://user-images.githubusercontent.com/19599562/194636095-28ebccc5-7ab9-49e7-a1a8-acd7b77c67cf.png) 10 | ![Screenshot-Windows](https://user-images.githubusercontent.com/19599562/193374122-f3d494d4-3872-44c6-9ef6-7cf67923f16b.png) 11 | ![Screenshot-Linux](https://user-images.githubusercontent.com/19599562/193374125-3b5d8d51-763a-40b0-b8f8-bb21c9a5f4fc.png) 12 | 13 | ### Functionality 14 | The "WeeApp" application will scan the local network for Belkin WeMo devices and list what's found. They can be switched on/off from the app and if a device changes state due to external activity (e.g. physical toggle or schedule) this will be reflected in the app UI due to the notification feature. 15 | 16 | Searching for new devices can take 5-6 seconds but the app benefits from the caching feature of the library so previously known devices will be displayed very quickly upon restart. 17 | 18 | * The "paper basket" will forget all devices and erase the disk cache. 19 | * The reload button will load known devices from cache and rescan the network. 20 | * The application remembers size and position of its window. 21 | 22 | ### Platforms 23 | Current version tested on Windows 11, Linux and maOS x84_64. 24 | 25 | ### Building 26 | 27 | The (example) App has certain FLTK_rs [dependencies](https://fltk-rs.github.io/fltk-book/Setup.html). 28 | 29 | ``` 30 | cargo build --release --example weeapp 31 | ``` 32 | #### Windows addendum 33 | An application icon can be found in `examples/images/icon.ico`. To insert it into the Windows binary use [rcedit][56bbd8db]: 34 | ``` 35 | rcedit target\release\examples\weeapp.exe --set-icon examples\images\icon.ico 36 | ``` 37 | [56bbd8db]: https://github.com/electron/rcedit/releases "rcedit" 38 | 39 | ## weectrl, the library 40 | ### Functionality 41 | * Discover devices on the network. 42 | * Retrieve detailed device information. 43 | * Switch devices on or off. 44 | * Cache known devices on disk for quick reload. 45 | * Subscription to state change notifications from devices if they're toggled. 46 | * Uses the Tracing crate for logging. 47 | 48 | ### API examples 49 | 50 | #### Initialization and discovery 51 | Create new instance of controller: 52 | ``` rust 53 | 54 | use weectrl::{DeviceInfo, DiscoveryMode, State, StateNotification, WeeController}; 55 | 56 | let controller = WeeController::new(); 57 | ``` 58 | 59 | To discover devices on network or/and in cache. starting by reading list of known devices from the disk cache, then broadcasts network query: 60 | ``` rust 61 | 62 | let rx: std::sync::mpsc::Receiver = controller.discover( 63 | DiscoveryMode::CacheAndBroadcast, 64 | true, 65 | Duration::from_secs(5), 66 | ); 67 | ``` 68 | Scans both disk cache file and network, will "forget" in-memory list first. Give network devices maximum 5 seconds to respond. 69 | When discovery ends the channel will be closed. 70 | 71 | Futures version of the discover function. 72 | ``` rust 73 | 74 | let notifications: futures::channel::mpsc::UnboundedReceiver = controller.discover_future( 75 | DiscoveryMode::CacheAndBroadcast, 76 | true, 77 | Duration::from_secs(5), 78 | ); 79 | ``` 80 | 81 | 82 | #### Switching and notifications 83 | 84 | Let `unique_id` be a `DeviceInfo::unique_id` returned by previous discovery. 85 | 86 | Starting listening for notifications from subscribed devices. If called several times only the most recent `rx` channel will receive notifications: 87 | ``` rust 88 | 89 | let rx: std::sync::mpsc::Receiver = controller.notify(); 90 | ``` 91 | Whenever a switch is toggled a message will appear on the Receiver. 92 | 93 | Futures version. 94 | ``` rust 95 | 96 | let notifications_fut: futures::channel::mpsc::UnboundedReceiver = controller.notify_future(); 97 | ``` 98 | 99 | Subscribe to notifications for a device and instruct WeeController to automatically refresh subscriptions every 120 seconds before they expire. 100 | As this causes a network request to be made to the switch a 5 second network timeout is set for the request in case the switch doesn't respond: 101 | ``` rust 102 | 103 | let result = controller.subscribe(&unique_id, Duration::from_secs(120), true, Duration::from_secs(5)); 104 | ``` 105 | 106 | Unsubscribe from notifications from one device: 107 | ``` rust 108 | 109 | let result = controller.unsubscribe(&unique_id, Duration::from_secs(5)); 110 | ``` 111 | 112 | Unsubscribe from all notifications: 113 | ``` rust 114 | 115 | controller.unsubscribe_all(); 116 | ``` 117 | 118 | To toggle a switch (on or off): 119 | ``` rust 120 | 121 | let result = controller.set_binary_state(unique_id, State::On, Duration::from_secs(5)); 122 | match result { 123 | Ok(state) => println!("Ok({:?})", state), 124 | Err(err) => println!("Err({:?})", err), 125 | } 126 | ``` 127 | 128 | To get a switch state: 129 | ``` rust 130 | 131 | let result = controller.get_binary_state(unique_id, Duration::from_secs(5)); 132 | match result { 133 | Ok(state) => println!("Ok({:?})", state), 134 | Err(err) => println!("Err({:?})", err), 135 | } 136 | ``` 137 | 138 | Retrive icons stored in a switch: 139 | ``` rust 140 | 141 | let list: Result, Error> = controller.get_icons(unique_id, Duration::from_secs(5)); 142 | ``` 143 | 144 | Retrieve structure with information stored about a switch, from the controller. This is the same structure returned earlier during Discovery: 145 | ``` rust 146 | 147 | let result = controller.get_info(unique_id); 148 | ``` 149 | 150 | 151 | ## Compatibility 152 | Theoretically supports and device advertising the `urn:Belkin:service:basicevent:1` service. Tested with Lightswitch and Socket. 153 | Currently only tested with Belkin WeMo LightSwitch and Socket. 154 | 155 | ## License 156 | 157 | Licensed under either of 158 | 159 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 160 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 161 | -------------------------------------------------------------------------------- /src/rpc.rs: -------------------------------------------------------------------------------- 1 | extern crate mime; 2 | 3 | use reqwest::header; 4 | use reqwest::header::HeaderMap; 5 | use tracing::info; 6 | 7 | use std::sync::mpsc; 8 | use std::time::Duration; 9 | 10 | pub use reqwest::{Method, StatusCode}; 11 | 12 | pub use crate::error::Error; 13 | 14 | use url::Url; 15 | 16 | #[derive(Debug)] 17 | pub struct SubscribeResponse { 18 | pub sid: String, 19 | pub timeout: u32, 20 | } 21 | 22 | struct RpcResponse { 23 | pub status: StatusCode, 24 | pub sid: Option, 25 | pub timeout: Option, 26 | } 27 | 28 | /// Perform a HTTP unsubscribe action to specified URL. 29 | pub fn unsubscribe_action( 30 | url: &Url, 31 | sid: &str, 32 | conn_timeout: Duration, 33 | ) -> Result { 34 | info!("Unsubscribe {}.", sid); 35 | 36 | let mut headers = header::HeaderMap::new(); 37 | headers.insert("SID", header::HeaderValue::from_str(sid)?); 38 | 39 | let method = reqwest::Method::from_bytes(b"UNSUBSCRIBE").unwrap(); 40 | 41 | let response = http_request(&method, &headers, url, "", conn_timeout)?; 42 | 43 | if response.status() != StatusCode::OK { 44 | info!("Unsubscribe {}, Error {}.", sid, response.status()); 45 | return Err(Error::InvalidResponse(response.status())); 46 | } 47 | 48 | info!("Unsubscribe {} ok.", sid); 49 | Ok(response.status()) 50 | } 51 | 52 | fn rpc_run( 53 | method: &Method, 54 | headers: &HeaderMap, 55 | url: &Url, 56 | conn_timeout: Duration, 57 | ) -> Result { 58 | info!("Request: {:?} {:?}", method.to_string(), &url); 59 | 60 | let response = http_request(method, headers, url, "", conn_timeout)?; 61 | 62 | if response.status() != StatusCode::OK { 63 | return Err(Error::InvalidResponse(response.status())); 64 | } 65 | 66 | let sid = if let Some(sid) = response.headers().get("SID") { 67 | Some(sid.to_str().map_err(|_| (Error::InvalidState))?.to_string()) 68 | } else { 69 | None 70 | }; 71 | 72 | let tim = if let Some(tim) = response.headers().get("TIMEOUT") { 73 | Some(tim.to_str().map_err(|_| (Error::InvalidState))?.to_string()) 74 | } else { 75 | None 76 | }; 77 | 78 | Ok(RpcResponse { 79 | status: response.status(), 80 | sid, 81 | timeout: tim, 82 | }) 83 | } 84 | 85 | /// Perform a HTTP subscribe action to specified URL, starting a new subscription. 86 | pub fn subscribe( 87 | url: &Url, 88 | sub_timeout: Duration, 89 | callback: &str, 90 | conn_timeout: Duration, 91 | ) -> Result { 92 | info!("Subscribe"); 93 | 94 | let method = Method::from_bytes(b"SUBSCRIBE").unwrap(); 95 | 96 | let mut headers = HeaderMap::new(); 97 | headers.insert("CALLBACK", header::HeaderValue::from_str(callback)?); 98 | headers.insert("NT", header::HeaderValue::from_str("upnp:event")?); 99 | headers.insert( 100 | "TIMEOUT", 101 | header::HeaderValue::from_str(&format!("Second-{}", sub_timeout.as_secs()))?, 102 | ); 103 | info!("REQ:"); 104 | info!("{:?}", headers); 105 | let rpcresponse = rpc_run(&method, &headers, url, conn_timeout)?; 106 | 107 | handle_subscription_response(rpcresponse) 108 | } 109 | 110 | pub fn http_get_text(url: &Url, conn_timeout: Duration) -> Result { 111 | let headers = HeaderMap::new(); 112 | 113 | let response = http_request(&reqwest::Method::GET, &headers, url, "", conn_timeout)?; 114 | 115 | if response.status() != StatusCode::OK { 116 | return Err(Error::InvalidResponse(response.status())); 117 | } 118 | 119 | Ok(response.text()?) 120 | } 121 | 122 | pub fn http_get_bytes(url: &Url, conn_timeout: Duration) -> Result, Error> { 123 | let headers = HeaderMap::new(); 124 | 125 | let response = http_request(&reqwest::Method::GET, &headers, url, "", conn_timeout)?; 126 | 127 | if response.status() != StatusCode::OK { 128 | return Err(Error::InvalidResponse(response.status())); 129 | } 130 | 131 | Ok(response.bytes()?.to_vec()) 132 | } 133 | 134 | fn handle_subscription_response(response: RpcResponse) -> Result { 135 | if response.status != StatusCode::OK { 136 | return Err(Error::InvalidResponse(response.status)); 137 | } 138 | 139 | let sid = match response.sid { 140 | Some(sid) => sid, 141 | None => return Err(Error::DeviceError), 142 | }; 143 | 144 | let timeout_string = match response.timeout { 145 | Some(timeout) => timeout, 146 | None => return Err(Error::DeviceError), 147 | }; 148 | 149 | let timeout = if timeout_string.starts_with("Second-") { 150 | let (_, number) = timeout_string.split_at("Second-".len()); 151 | number.parse::().unwrap_or(0) 152 | } else { 153 | 0 154 | }; 155 | 156 | Ok(SubscribeResponse { sid, timeout }) 157 | } 158 | 159 | /// Perform a HTTP SOAP action to specified URL. 160 | pub fn soap_action( 161 | url: &Url, 162 | action: &str, 163 | xml: &str, 164 | conn_timeout: Duration, 165 | ) -> Result { 166 | info!("soap_action, url: {:?}", url.to_string()); 167 | 168 | let method = Method::GET; 169 | 170 | let mut headers = HeaderMap::new(); 171 | 172 | headers.insert("SOAPACTION", header::HeaderValue::from_str(action)?); 173 | 174 | headers.insert( 175 | reqwest::header::CONTENT_LENGTH, 176 | header::HeaderValue::from_str(&xml.len().to_string())?, 177 | ); 178 | 179 | headers.insert( 180 | reqwest::header::CONTENT_TYPE, 181 | header::HeaderValue::from_static("text/xml; charset=utf-8"), 182 | ); 183 | headers.insert( 184 | reqwest::header::CONNECTION, 185 | header::HeaderValue::from_str("close")?, 186 | ); 187 | 188 | let response = http_request(&method, &headers, url, xml, conn_timeout)?; 189 | 190 | if response.status() != StatusCode::OK { 191 | return Err(Error::InvalidResponse(response.status())); 192 | } 193 | 194 | Ok(response.text()?) 195 | } 196 | 197 | // Blocking Reqwest runs an async executor, if the app using this library does the same 198 | // the reactors will be nested and request will explode horribly. 199 | // So we give request its own thread. 200 | pub fn http_request( 201 | method: &Method, 202 | headers: &HeaderMap, 203 | url: &Url, 204 | body: &str, 205 | conn_timeout: Duration, 206 | ) -> Result { 207 | let (tx, rx) = mpsc::channel(); 208 | 209 | let (method, headers, url, body) = ( 210 | method.clone(), 211 | headers.clone(), 212 | url.clone(), 213 | body.to_string(), 214 | ); 215 | 216 | std::thread::spawn(move || { 217 | let client = reqwest::blocking::Client::new(); 218 | 219 | let request = reqwest::blocking::Client::request(&client, method.clone(), url.clone()) 220 | .headers(headers.clone()) 221 | .body(body.to_string()) 222 | .timeout(conn_timeout); 223 | let response = request.send(); 224 | let _ignore = tx.send(response.map_err(|e| Error::from(e))); 225 | }); 226 | 227 | rx.recv().unwrap() 228 | } 229 | -------------------------------------------------------------------------------- /src/device.rs: -------------------------------------------------------------------------------- 1 | use std::net::IpAddr; 2 | use std::str::FromStr; 3 | use std::sync::mpsc; 4 | use std::sync::{Arc, Mutex}; 5 | use std::{thread, time::Duration}; 6 | use tracing::info; 7 | use url::Url; 8 | 9 | use crate::error; 10 | use crate::error::Error; 11 | use crate::rpc; 12 | use crate::weectrl::{DeviceInfo, Icon, State}; 13 | use crate::xml; 14 | 15 | #[derive(Debug, Clone)] 16 | /// One WeMo or similar device 17 | pub struct Device { 18 | /// IP address of local interface on the same network as the "remote" device. 19 | local_ip: IpAddr, 20 | /// Path for retrieve and toggle switch state. 21 | binary_control_path: Option, 22 | /// Path for subscribing to events 23 | event_path: Option, 24 | /// Subscription ID for notifications, can be updated from daemon thread if it changes. 25 | sid: Option>>, 26 | /// If subscribed for notifications this is used to cancel th daemon. 27 | subscription_daemon: Option>, 28 | // Information about device, including that returned by 'home' url. 29 | pub info: DeviceInfo, 30 | } 31 | 32 | impl Device { 33 | pub fn new(info: DeviceInfo, local_ip: IpAddr) -> Self { 34 | let dev = Self { 35 | local_ip, 36 | binary_control_path: None, 37 | event_path: None, 38 | sid: None, 39 | subscription_daemon: None, 40 | info, 41 | }; 42 | info!( 43 | "New device {}, {} @ {}", 44 | dev.info.root.device.friendly_name, dev.info.root.device.mac_address, dev.info.base_url 45 | ); 46 | dev 47 | } 48 | 49 | /// Returns copy of current subscription ID. 50 | pub fn sid(&self) -> Option { 51 | if let Some(dev_sid) = self.sid.clone() { 52 | let sid = dev_sid.lock().expect(error::FATAL_LOCK).clone(); 53 | return Some(sid); 54 | } 55 | None 56 | } 57 | 58 | /// Any supported device needs to have the Belkin basicevent service. Check for its 59 | /// existence and save the paths for control/subscriptions for future use. 60 | pub fn validate_device(&mut self) -> bool { 61 | for service in &self.info.root.device.service_list.service { 62 | if service.service_type == "urn:Belkin:service:basicevent:1" { 63 | self.binary_control_path = Some(service.control_url.clone()); 64 | self.event_path = Some(service.event_sub_url.clone()); 65 | return true; 66 | } 67 | } 68 | false 69 | } 70 | 71 | fn cancel_subscription_daemon(&mut self) { 72 | if let Some(daemon) = self.subscription_daemon.clone() { 73 | info!("Cancel subscription daemon for {}", self.info.friendly_name); 74 | let _ = daemon.send(()); 75 | self.subscription_daemon = None; 76 | } 77 | } 78 | 79 | /// Send unsubscribe command and cancel the daemon if active. 80 | pub fn unsubscribe(&mut self, conn_timeout: Duration) -> Result { 81 | info!("UnSubscribe {} {:?}", self.info.friendly_name, self.sid); 82 | self.cancel_subscription_daemon(); 83 | 84 | if let Some(sid_shared) = self.sid.clone() { 85 | let req_url = Self::make_request_url(&self.info.base_url, &self.event_path)?; 86 | let sid = sid_shared.lock().expect(error::FATAL_LOCK).clone(); 87 | let _ = rpc::unsubscribe_action(&req_url, &sid, conn_timeout)?; // TODO check statuscode 88 | self.sid = None; 89 | return Ok(sid); 90 | } 91 | Err(Error::NotSubscribed) 92 | } 93 | 94 | // Concatenate basic URL with request path. 95 | // e.g. "http://192.168.0.10/ with "upnp/event/basicevent1". 96 | fn make_request_url(base_url: &str, req_path: &Option) -> Result { 97 | if let Some(ref path) = *req_path { 98 | let mut req_url = base_url.to_owned(); 99 | if req_url.ends_with('/') && path.starts_with('/') { 100 | req_url = req_url.trim_end_matches('/').to_owned(); 101 | } 102 | req_url.push_str(path); 103 | return Ok(Url::from_str(&req_url)?); 104 | } 105 | Err(Error::UnsupportedDevice) 106 | } 107 | 108 | /// Send subscribe comand, cancel any currrent daemon and start a new if auto resub on. 109 | pub fn subscribe( 110 | &mut self, 111 | port: u16, 112 | seconds: Duration, 113 | auto_resubscribe: bool, 114 | conn_timeout: Duration, 115 | ) -> Result<(String, u32), Error> { 116 | info!("Subscribe"); 117 | let callback = format!("", self.local_ip, port); 118 | let req_url = Self::make_request_url(&self.info.base_url, &self.event_path)?; 119 | self.cancel_subscription_daemon(); 120 | let res = rpc::subscribe(&req_url, seconds, &callback, conn_timeout)?; 121 | 122 | let new_sid = Arc::new(Mutex::new(res.sid.clone())); 123 | self.sid = Some(new_sid.clone()); 124 | 125 | if auto_resubscribe { 126 | let (tx, rx) = mpsc::channel(); 127 | self.subscription_daemon = Some(tx); 128 | 129 | std::thread::Builder::new() 130 | .name(format!("DEV_resub_thread {}", self.info.friendly_name)) 131 | .spawn(move || { 132 | Self::subscription_daemon(&rx, &req_url, &new_sid, seconds, &callback); 133 | })?; 134 | } 135 | 136 | Ok((res.sid, res.timeout)) 137 | } 138 | 139 | fn subscription_daemon( 140 | rx: &mpsc::Receiver<()>, 141 | url: &Url, 142 | device_sid: &Arc>, 143 | initial_seconds: Duration, 144 | callback: &str, 145 | ) { 146 | use std::sync::mpsc::TryRecvError; 147 | 148 | let mut duration = initial_seconds - Duration::from_secs(10); 149 | 150 | loop { 151 | thread::sleep(duration); 152 | // Were we signalled to end, or channel nonfunctional? 153 | match rx.try_recv() { 154 | Err(TryRecvError::Empty) => (), 155 | _ => break, 156 | } 157 | info!("Resubscribing."); 158 | match rpc::subscribe(url, initial_seconds, callback, Duration::from_secs(5)) { 159 | Ok(res) => { 160 | duration = Duration::from_secs(u64::from(res.timeout - 10)); // Switch returns timeout we need to obey. 161 | let mut sid_ref = device_sid.lock().expect(error::FATAL_LOCK); 162 | sid_ref.clear(); 163 | sid_ref.push_str(&res.sid); 164 | } 165 | Err(e) => { 166 | info!("Err: {:?}", e); 167 | continue; // Could be error due to temporary network issue, just keep trying after a bit. 168 | } 169 | }; 170 | } 171 | } 172 | 173 | /// Retrieve current switch binarystate. 174 | pub fn fetch_icons(&mut self, conn_timeout: Duration) -> Result, Error> { 175 | let mut icon_list = Vec::new(); 176 | 177 | self.info.state = State::Unknown; 178 | 179 | for xmlicon in &self.info.root.device.icon_list.icon { 180 | let width = xmlicon.width.parse::()?; 181 | let height = xmlicon.height.parse::()?; 182 | let depth = xmlicon.depth.parse::()?; 183 | 184 | let req_file = Some(xmlicon.url.clone()); 185 | let req_url = Self::make_request_url(&self.info.base_url, &req_file)?; 186 | 187 | let data = rpc::http_get_bytes(&req_url, conn_timeout)?; 188 | 189 | // Image type is unreliable, so we have to peek in the data stream to detect it. 190 | let mimetype: mime::Mime = match data.as_slice() { 191 | // JPEG 192 | [0xFF, 0xD8, 0xFF, ..] => mime::IMAGE_JPEG, 193 | // GIF 194 | [0x47, 0x49, 0x46, 0x38, ..] => mime::IMAGE_GIF, 195 | // PNG 196 | [0x89, 0x50, 0x4E, 0x47, ..] => mime::IMAGE_PNG, 197 | _ => return Err(Error::InvalidResponse(reqwest::StatusCode::IM_A_TEAPOT)), 198 | }; 199 | 200 | info!( 201 | "Fetch Icon {}. MimeType returned: {:#?}", 202 | &req_url, &mimetype 203 | ); 204 | 205 | let icon = Icon { 206 | mimetype, 207 | width, 208 | height, 209 | depth, 210 | data, 211 | }; 212 | 213 | icon_list.push(icon); 214 | } 215 | 216 | Ok(icon_list) 217 | } 218 | 219 | /// Retrieve current switch binarystate. 220 | pub fn fetch_binary_state(&mut self, conn_timeout: Duration) -> Result { 221 | self.info.state = State::Unknown; 222 | 223 | let req_url = Self::make_request_url(&self.info.base_url, &self.binary_control_path)?; 224 | 225 | let http_response = rpc::soap_action( 226 | &req_url, 227 | "\"urn:Belkin:service:basicevent:1#GetBinaryState\"", 228 | xml::GETBINARYSTATE, 229 | conn_timeout, 230 | )?; 231 | 232 | if let Some(state) = xml::get_binary_state(&http_response) { 233 | self.info.state = State::from(state); 234 | return Ok(self.info.state); 235 | } 236 | 237 | return Err(Error::InvalidState); 238 | } 239 | 240 | /// Send command to toggle switch state. If toggeling to same state an Error is returned. 241 | pub fn set_binary_state( 242 | &mut self, 243 | state: State, 244 | conn_timeout: Duration, 245 | ) -> Result { 246 | if state == State::Unknown { 247 | return Err(Error::InvalidState); 248 | } 249 | 250 | let req_url = Self::make_request_url(&self.info.base_url, &self.binary_control_path)?; 251 | 252 | rpc::soap_action( 253 | &req_url, 254 | "\"urn:Belkin:service:basicevent:1#SetBinaryState\"", 255 | &state.to_string(), 256 | conn_timeout, 257 | )?; 258 | 259 | self.info.state = state; 260 | Ok(state) 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/weectrl.rs: -------------------------------------------------------------------------------- 1 | use hyper::body::HttpBody; 2 | use hyper::service::{make_service_fn, service_fn}; 3 | use hyper::{Body, HeaderMap, Request, Response, Server}; 4 | use std::convert::Infallible; 5 | use std::net::SocketAddr; 6 | use std::str::FromStr; 7 | use std::time::Duration; 8 | use tracing::info; 9 | 10 | use futures::channel::mpsc as mpsc_future; 11 | use std::collections::hash_map::Entry; 12 | use std::collections::HashMap; 13 | use std::sync::{mpsc, Arc, Mutex}; 14 | 15 | use std::net::IpAddr; 16 | use std::net::TcpListener; 17 | use std::net::UdpSocket; 18 | 19 | use crate::cache::{DeviceAddress, DiskCache}; 20 | use crate::device::Device; 21 | use crate::error; 22 | use crate::error::{Error, FATAL_LOCK}; 23 | use crate::xml; 24 | pub use mime::Mime; 25 | use url::Url; 26 | use xml::Root; 27 | 28 | #[derive(Debug, Clone)] 29 | /// Notification from a device on network that binary state have changed. 30 | pub struct StateNotification { 31 | /// Same unique id as known from discovery and used to subscribe. 32 | pub unique_id: String, 33 | /// New binarystate of the device. 34 | pub state: State, 35 | } 36 | 37 | impl From for State { 38 | fn from(u: u8) -> Self { 39 | match u { 40 | 0 => Self::Off, 41 | 1 => Self::On, 42 | _ => Self::Unknown, 43 | } 44 | } 45 | } 46 | 47 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 48 | /// Specify the methods to be used for device discovery. 49 | pub enum DiscoveryMode { 50 | /// Read and verify known devices from disk cache. Don't brodcast uPnP query. 51 | CacheOnly, 52 | /// Read and verify known devices from disk cache, then brodcast uPnP query. 53 | CacheAndBroadcast, 54 | /// Broadcast uPnP query, don't load devices from disk cache. This will cause 55 | /// disk cache to be overwritten with new list of devices. 56 | BroadcastOnly, 57 | } 58 | 59 | #[derive(Debug, Clone, PartialEq, Eq)] 60 | /// Type of device 61 | pub enum Model { 62 | Lightswitch, 63 | Socket, 64 | Unknown(String), 65 | } 66 | 67 | impl std::fmt::Display for Model { 68 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 69 | match self { 70 | Self::Lightswitch => write!(f, "Light Switch"), 71 | Self::Socket => write!(f, "Socket"), 72 | Self::Unknown(str) => write!(f, "{}", str), 73 | } 74 | } 75 | } 76 | 77 | impl<'a> From<&'a str> for Model { 78 | fn from(string: &'a str) -> Self { 79 | match string { 80 | "LightSwitch" => Self::Lightswitch, 81 | "Socket" => Self::Socket, 82 | _ => Self::Unknown(string.to_owned()), 83 | } 84 | } 85 | } 86 | 87 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 88 | /// Represents whether a device binary state is on or off. 89 | pub enum State { 90 | /// Device is switched on 91 | On, 92 | /// Device is switched off 93 | Off, 94 | /// Device is not responding to queries or commands 95 | Unknown, 96 | } 97 | 98 | impl std::fmt::Display for State { 99 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 100 | match *self { 101 | Self::On => f.write_str(xml::SETBINARYSTATEON), 102 | Self::Off => f.write_str(xml::SETBINARYSTATEOFF), 103 | Self::Unknown => f.write_str("UNKNOWN STATE"), 104 | } 105 | } 106 | } 107 | 108 | #[derive(Debug, Clone)] 109 | // A WeMo swicth have an associated icon 110 | pub struct Icon { 111 | pub mimetype: Mime, 112 | pub width: u64, 113 | pub height: u64, 114 | pub depth: u64, 115 | pub data: Vec, 116 | } 117 | 118 | #[derive(Debug, Clone)] 119 | // Data returned for each device found during discovery. 120 | pub struct DeviceInfo { 121 | /// Human readable name returned from the device homepage. 122 | pub friendly_name: String, 123 | /// Basic model name, e.g. LightSwitch, Socket. 124 | pub model: Model, 125 | /// Unique identifier for this device, used for issuing commands to controller. 126 | pub unique_id: String, 127 | /// Network hostname, usually the IP address. 128 | pub hostname: String, 129 | /// http address including port number 130 | pub base_url: String, 131 | /// Device "home" URL 132 | pub location: String, 133 | /// Current switch binarystate 134 | pub state: State, 135 | /// Device information, from XML homepage 136 | pub root: Root, 137 | /// Device information, data from XML homepage 138 | pub xml: String, 139 | } 140 | 141 | /// Controller entity used for finding and controlling Belkin WeMo, and compatible, devices. 142 | pub struct WeeController { 143 | cache: Arc>, 144 | devices: Arc>>, 145 | notification_mpsc: Arc>>>, 146 | port: u16, 147 | } 148 | 149 | impl Default for WeeController { 150 | fn default() -> Self { 151 | Self::new() 152 | } 153 | } 154 | 155 | impl Drop for WeeController { 156 | fn drop(&mut self) { 157 | info!("Dropping WeeController"); 158 | self.unsubscribe_all(); 159 | } 160 | } 161 | 162 | impl WeeController { 163 | #[must_use] 164 | pub fn new() -> Self { 165 | info!("Init"); 166 | 167 | // Find a free port number between 8000-9000 168 | let port = Self::get_available_port().unwrap(); 169 | let notification_mpsc = Arc::new(Mutex::new(None)); 170 | let devices = Arc::new(Mutex::new(HashMap::new())); 171 | 172 | // Start the webserver listeninh for incoming notifications 173 | let tx = notification_mpsc.clone(); 174 | let devs = devices.clone(); 175 | let _ignore = std::thread::Builder::new() 176 | .name("SUB_server".to_string()) 177 | .spawn(move || { 178 | Self::notification_server(tx, devs, port); 179 | }) 180 | .expect("UNABLE TO START NOTFICATION THREAD"); 181 | 182 | // Build self 183 | Self { 184 | cache: Arc::new(Mutex::new(DiskCache::new())), 185 | devices, 186 | notification_mpsc, 187 | port, 188 | } 189 | } 190 | 191 | /// Forget all known devices, optionalle also clear the on-disk cache. 192 | pub fn clear(&self, clear_cache: bool) { 193 | info!("Clearing list of devices. Clear disk; {:?}.", clear_cache); 194 | self.unsubscribe_all(); 195 | if clear_cache { 196 | self.cache.lock().expect(FATAL_LOCK).clear(); 197 | } 198 | self.devices.lock().expect(FATAL_LOCK).clear(); 199 | } 200 | 201 | /// Set the device binary state (On/Off), e.g. toggle a switch. 202 | pub fn set_binary_state( 203 | &self, 204 | unique_id: &str, 205 | state: State, 206 | conn_timeout: Duration, 207 | ) -> Result { 208 | info!("Set binary state for Device {:?} {:?} ", unique_id, state); 209 | let mut devices = self.devices.lock().expect(FATAL_LOCK); 210 | if let Entry::Occupied(mut o) = devices.entry(unique_id.to_owned()) { 211 | let device = o.get_mut(); 212 | return device.set_binary_state(state, conn_timeout); 213 | } 214 | Err(Error::UnknownDevice) 215 | } 216 | 217 | /// Query the device for the binary state (On/Off) 218 | pub fn get_binary_state( 219 | &self, 220 | unique_id: &str, 221 | conn_timeout: Duration, 222 | ) -> Result { 223 | info!("Get_binary_state for device {:?}.", unique_id); 224 | let mut devices = self.devices.lock().expect(FATAL_LOCK); 225 | if let Entry::Occupied(mut o) = devices.entry(unique_id.to_owned()) { 226 | let device = o.get_mut(); 227 | return device.fetch_binary_state(conn_timeout); 228 | } 229 | Err(Error::UnknownDevice) 230 | } 231 | 232 | /// Query the device for a list of icons 233 | pub fn get_icons(&self, unique_id: &str, conn_timeout: Duration) -> Result, Error> { 234 | info!("Get_icons for device {:?}.", unique_id); 235 | let mut devices = self.devices.lock().expect(FATAL_LOCK); 236 | if let Entry::Occupied(mut o) = devices.entry(unique_id.to_owned()) { 237 | let device = o.get_mut(); 238 | return device.fetch_icons(conn_timeout); 239 | } 240 | Err(Error::UnknownDevice) 241 | } 242 | 243 | /// Return the cached DeviceInfo structure, which was retrieved during discovery. 244 | pub fn get_info(&self, unique_id: &str) -> Result { 245 | info!("Get information for device {:?}.", unique_id); 246 | let mut devices = self.devices.lock().expect(FATAL_LOCK); 247 | if let Entry::Occupied(mut o) = devices.entry(unique_id.to_owned()) { 248 | let device = o.get_mut(); 249 | return Ok(device.info.clone()); 250 | } 251 | Err(Error::UnknownDevice) 252 | } 253 | 254 | /// Cancel subscription for notifications from all devices. 255 | // @TODO spin off in threads? 256 | pub fn unsubscribe_all(&self) { 257 | info!("unsubscribe_all."); 258 | let mut devices = self.devices.lock().expect(FATAL_LOCK); 259 | let unique_ids: Vec = devices.keys().cloned().collect(); 260 | let mut handles = Vec::with_capacity(10); 261 | 262 | for unique_id in unique_ids { 263 | if let Entry::Occupied(mut o) = devices.entry(unique_id.clone()) { 264 | let mut device = o.get_mut().clone(); 265 | handles.push(std::thread::spawn(move || { 266 | info!("unsubscribe {:?}.", unique_id); 267 | let _ignore = device.unsubscribe(Duration::from_secs(5)); 268 | })); 269 | } 270 | } 271 | for handle in handles { 272 | let _ignore = handle.join(); 273 | } 274 | info!("Done unsubscribe_all."); 275 | } 276 | 277 | /// Cancel subscription for notifications from a device. 278 | pub fn unsubscribe(&self, unique_id: &str, conn_timeout: Duration) -> Result { 279 | let mut devices = self.devices.lock().expect(FATAL_LOCK); 280 | if let Entry::Occupied(mut o) = devices.entry(unique_id.to_owned()) { 281 | let device = o.get_mut(); 282 | 283 | info!("unsubscribe {:?}.", unique_id); 284 | return device.unsubscribe(conn_timeout); 285 | } 286 | Err(Error::UnknownDevice) 287 | } 288 | 289 | /// Subscribe to a device for notifications on state change. 290 | /// Typically 120-600 seconds. If auto_resubscribe==true the subscription will 291 | /// be renewed automatically. 292 | /// Notifications will be returned via the mpsc returned from start_subscription_service. 293 | pub fn subscribe( 294 | &self, 295 | unique_id: &str, 296 | seconds: Duration, 297 | auto_resubscribe: bool, 298 | conn_timeout: Duration, 299 | ) -> Result { 300 | if seconds < Duration::from_secs(15) { 301 | info!("Subscribe: TimeoutTooShort"); 302 | return Err(Error::TimeoutTooShort); 303 | } 304 | 305 | let mut devices = self.devices.lock().expect(FATAL_LOCK); 306 | if let Entry::Occupied(mut o) = devices.entry(unique_id.to_owned()) { 307 | let device = o.get_mut(); 308 | info!("subscribe {:?}.", unique_id); 309 | let (_, time) = device.subscribe(self.port, seconds, auto_resubscribe, conn_timeout)?; 310 | return Ok(time); 311 | } 312 | info!("Subscribe: UnknownDevice"); 313 | Err(Error::UnknownDevice) 314 | } 315 | 316 | /// Read list of know devices from disk cache as well as new devices responding on the network. 317 | /// Returns immediately and send discovered devices back on the mpsc future as they're found. 318 | /// Allow network devices max `mx` seconds to respond. 319 | /// When discovery ends, after mx seconds and a bit, the channel will be closed. 320 | /// `forget_devices` = true will clear the internal list of devices. 321 | /// Futures alternative to fn Discover(...) 322 | pub fn discover_future( 323 | &self, 324 | mode: DiscoveryMode, 325 | forget_devices: bool, 326 | mx: Duration, 327 | ) -> mpsc_future::UnboundedReceiver { 328 | let rx = self.discover(mode, forget_devices, mx); 329 | 330 | let (tx_fut, rx_fut) = mpsc_future::unbounded(); 331 | 332 | // Just wrap the standard mpsc version, easy with unbounded channels 333 | let _ = std::thread::Builder::new() 334 | .name("Discovery future proxy".to_string()) 335 | .spawn(move || { 336 | info!("Starting discovery future proxy thread"); 337 | while let Ok(device) = rx.recv() { 338 | info!("Future thread Sending {}.", device.friendly_name); 339 | let _ = tx_fut.unbounded_send(device); 340 | } 341 | }); 342 | rx_fut 343 | } 344 | 345 | /// Read list of know devices from disk cache as well as new devices responding on the network. 346 | /// Returns immediately and send discovered devices back on the mpsc as they're found. 347 | /// Allow network devices max `mx` seconds to respond. 348 | /// When discovery ends, after mx seconds and a bit, the channel will be closed. 349 | /// `forget_devices` = true will clear the internal list of devices. 350 | pub fn discover( 351 | &self, 352 | mode: DiscoveryMode, 353 | forget_devices: bool, 354 | mx: Duration, 355 | ) -> mpsc::Receiver { 356 | if forget_devices { 357 | self.clear(false); 358 | } 359 | 360 | let (tx, rx) = mpsc::channel(); 361 | let devices = self.devices.clone(); 362 | let cache = self.cache.clone(); 363 | 364 | let _ = std::thread::Builder::new() 365 | .name("SSDP_main".to_string()) 366 | .spawn(move || { 367 | info!("Starting discover"); 368 | 369 | if mode == DiscoveryMode::CacheOnly || mode == DiscoveryMode::CacheAndBroadcast { 370 | info!("Loading devices from cache."); 371 | if let Some(cache_list) = cache.lock().expect(error::FATAL_LOCK).read() { 372 | info!("Cached devices {:?}", cache_list); 373 | 374 | for cache_entry in cache_list { 375 | let device = 376 | Self::register_device(&cache_entry.location, &devices).ok(); 377 | if let Some(new) = device { 378 | let _ignore = tx.send(new); 379 | } 380 | } 381 | } 382 | } 383 | 384 | if mode == DiscoveryMode::BroadcastOnly || mode == DiscoveryMode::CacheAndBroadcast 385 | { 386 | info!("Broadcasting uPnP query."); 387 | // Create Our Search Request 388 | 389 | let devices = devices.clone(); // to move into the new thread 390 | let mut cache_dirty = false; 391 | 392 | let bind_address = Self::get_bind_addr().unwrap(); 393 | let socket = UdpSocket::bind(&bind_address).unwrap(); 394 | 395 | Self::send_sddp_request(&socket, mx); 396 | 397 | let mut buf = [0u8; 2048]; 398 | socket 399 | .set_read_timeout(Some(mx + Duration::from_secs(1))) 400 | .unwrap(); 401 | 402 | let (cache_dirt_tx, cache_dirt_rx) = mpsc::channel(); 403 | 404 | let mut num_threads = 0; 405 | while let Ok(e) = socket.recv(&mut buf) { 406 | let message = std::str::from_utf8(&buf[..e]).unwrap().to_string(); 407 | 408 | // Handle message in different thread 409 | let devs = devices.clone(); 410 | let nx = tx.clone(); 411 | let cache_dirt = cache_dirt_tx.clone(); 412 | num_threads += 1; 413 | 414 | let _ignore = std::thread::Builder::new() 415 | .name(format!("SSDP_handle_msg {}", num_threads).to_string()) 416 | .spawn(move || { 417 | if let Some(location) = Self::parse_ssdp_response(&message) { 418 | let device = Self::register_device(&location, &devs).ok(); 419 | 420 | device.map_or_else( 421 | || { 422 | let _ignore = cache_dirt.send(false); 423 | }, 424 | |new| { 425 | let _ignore = nx.send(new); 426 | let _ignore = cache_dirt.send(true); 427 | }, 428 | ); 429 | } 430 | }); 431 | } 432 | 433 | for _ in 0..num_threads { 434 | if cache_dirt_rx.recv().unwrap() { 435 | info!("CACHE DIRTY"); 436 | cache_dirty = true; 437 | } 438 | } 439 | 440 | if cache_dirty { 441 | Self::refresh_cache(&cache, &devices); 442 | } 443 | } 444 | 445 | info!("Done! Ending discover thread."); 446 | }); 447 | rx 448 | } 449 | 450 | /// Retrieve a mpsc future (stream) on which to get notifications. 451 | /// Futures alternative to get_notifications. 452 | pub fn notify_future(&self) -> mpsc_future::UnboundedReceiver { 453 | let rx = self.notify(); 454 | 455 | let (tx_fut, rx_fut) = mpsc_future::unbounded(); 456 | 457 | // Just wrap the standard mpsc version, easy with unbounded channels 458 | let _ = std::thread::Builder::new() 459 | .name("subscription future proxy".to_string()) 460 | .spawn(move || { 461 | info!("Starting subscription future proxy thread"); 462 | while let Ok(notification) = rx.recv() { 463 | let _ = tx_fut.unbounded_send(notification); 464 | } 465 | }); 466 | rx_fut 467 | } 468 | 469 | /// Retrieve a mpsc on which to get notifications. 470 | pub fn notify(&self) -> mpsc::Receiver { 471 | let (tx, rx) = mpsc::channel::(); 472 | let mut notification_mpsc = self.notification_mpsc.lock().expect(FATAL_LOCK); 473 | *notification_mpsc = Some(tx); 474 | rx 475 | } 476 | 477 | // Get the IP address of this PC, from the same interface talking to the device. 478 | // It will be needed if subscriptions are used. It's not pretty but easiest way to Make 479 | // sure we can talk to multiple devices on separate interfaces. 480 | fn get_local_ip(location: &str) -> Result { 481 | use std::net::TcpStream; 482 | 483 | // extract ip:port from URL 484 | let location_url = Url::parse(location)?; 485 | if let Some(host) = location_url.host_str() { 486 | let mut destination = String::from(host); 487 | if let Some(port) = location_url.port() { 488 | let port_str = format!(":{}", port); 489 | destination.push_str(&port_str); 490 | } 491 | 492 | // Create TCP connection from which we can get local IP. 493 | let destination_str: &str = &destination; 494 | let stream = TcpStream::connect(&destination_str)?; 495 | let loc_ip = stream.local_addr()?; 496 | return Ok(loc_ip.ip()); 497 | } 498 | Err(Error::NoResponse) 499 | } 500 | 501 | // Retrieve a device home page. 502 | fn get_device_home(location: &Url, conn_timeout: Duration) -> Result { 503 | let response = super::rpc::http_get_text(location, conn_timeout)?; 504 | Ok(response) 505 | } 506 | 507 | // Write list of active devices to disk cache 508 | fn refresh_cache(cache: &Arc>, devices: &Arc>>) { 509 | info!("Refreshing cache."); 510 | let mut list = Vec::new(); 511 | for (mac, dev) in devices.lock().expect(FATAL_LOCK).iter() { 512 | let location = dev.info.location.clone(); 513 | list.push(DeviceAddress { 514 | location, 515 | mac_address: mac.clone(), 516 | }); 517 | } 518 | cache.lock().expect(FATAL_LOCK).write(list); 519 | } 520 | 521 | // Given location URL, query a device. If successful add to list of active devices 522 | fn retrieve_device(location: &str, conn_timeout: Duration) -> Result { 523 | let mut url = Url::from_str(location)?; 524 | let xml = Self::get_device_home(&url, conn_timeout)?; 525 | let local_ip = Self::get_local_ip(location)?; 526 | let root: Root = xml::parse_services(&xml)?; 527 | 528 | info!("Device {:?} {:?} ", root.device.friendly_name, location); 529 | 530 | let mut hostname = String::new(); 531 | //let mut base_url = Url::parse(location)?; 532 | if let Some(hn) = url.host_str() { 533 | hostname = hn.to_owned(); 534 | } 535 | url.set_path("/"); 536 | 537 | let model = root.device.model_name.clone(); 538 | let model_str: &str = &model; 539 | let info = DeviceInfo { 540 | friendly_name: root.device.friendly_name.clone(), 541 | model: Model::from(model_str), 542 | unique_id: root.device.mac_address.clone(), 543 | hostname, 544 | base_url: url.to_string(), 545 | location: location.to_string(), 546 | state: State::Unknown, 547 | root, 548 | xml, 549 | }; 550 | let mut dev = Device::new(info, local_ip); 551 | 552 | if dev.validate_device() 553 | && dev 554 | .fetch_binary_state(Duration::from_secs(5)) 555 | .ok() 556 | .is_some() 557 | { 558 | return Ok(dev); 559 | } 560 | 561 | info!("Device not supported."); 562 | Err(Error::UnsupportedDevice) 563 | } 564 | 565 | // Add device to hashmap. 566 | fn register_device( 567 | location: &str, 568 | devices: &Arc>>, 569 | ) -> Result { 570 | let newdev = Self::retrieve_device(location, Duration::from_secs(5))?; 571 | let mut devs = devices.lock().expect(FATAL_LOCK); 572 | let unique_id = newdev.info.root.device.mac_address.clone(); 573 | 574 | if let std::collections::hash_map::Entry::Vacant(e) = devs.entry(unique_id) { 575 | info!("Registering dev: {:?}", newdev.info.unique_id); 576 | let info = newdev.info.clone(); 577 | e.insert(newdev); 578 | return Ok(info); 579 | } 580 | Err(Error::DeviceAlreadyRegistered) 581 | } 582 | 583 | async fn http_req_handler( 584 | notification_mspsc: Arc>>>, 585 | devices: Arc>>, 586 | req: Request, 587 | ) -> Result, Infallible> { 588 | let (parts, mut stream) = req.into_parts(); 589 | 590 | // Retrieve the entire message body 591 | let mut body = String::new(); 592 | while let Some(d) = stream.data().await { 593 | if let Ok(s) = d { 594 | body += std::str::from_utf8(&s).unwrap(); 595 | } 596 | } 597 | 598 | // Ok full message received, now process it 599 | if body.len() > 0 && body.find("") != None { 600 | // Extract SID and BinaryState 601 | if let Some((notification_sid, state)) = Self::get_sid_and_state(&body, &parts.headers) 602 | { 603 | info!("str, sid {notification_sid} {}", state); 604 | // If we have mathing SID, register and forward statechange 605 | let devs = devices.lock().expect(FATAL_LOCK); 606 | 'search: for (unique_id, dev) in devs.iter() { 607 | if let Some(device_sid) = dev.sid() { 608 | // Found a match, send notification to the client 609 | if notification_sid == device_sid { 610 | info!( 611 | "Server got switch update for {:?}. sid: {:?}. State: {:?}.", 612 | unique_id, 613 | notification_sid, 614 | State::from(state) 615 | ); 616 | 617 | let n = StateNotification { 618 | unique_id: unique_id.clone(), 619 | state: State::from(state), 620 | }; 621 | 622 | let tx = notification_mspsc.lock().expect(FATAL_LOCK); 623 | match &*tx { 624 | None => info!("No listener, dropping notification."), 625 | Some(tx) => { 626 | let _r = tx.send(n); 627 | } 628 | } 629 | break 'search; 630 | } 631 | } 632 | } 633 | } 634 | } 635 | 636 | Ok(Response::new(Body::from(""))) 637 | } 638 | 639 | #[tokio::main()] 640 | async fn notification_server( 641 | notification_mspsc: Arc>>>, 642 | devices: Arc>>, 643 | port: u16, 644 | ) { 645 | // A `MakeService` that produces a `Service` to handle each connection. 646 | let make_service = make_service_fn(move |_| { 647 | let notification_mspsc = notification_mspsc.clone(); 648 | let devices = devices.clone(); 649 | 650 | // Create a `Service` for responding to the request. 651 | let service = service_fn(move |req| { 652 | Self::http_req_handler(notification_mspsc.clone(), devices.clone(), req) 653 | }); 654 | 655 | // Return the service to hyper. 656 | async move { Ok::<_, Infallible>(service) } 657 | }); 658 | 659 | let addr: SocketAddr = ([0, 0, 0, 0], port).into(); 660 | 661 | info!("Listening to notifications on: {:?}", addr.to_string()); 662 | 663 | let server = Server::bind(&addr).serve(make_service); 664 | 665 | if let Err(e) = server.await { 666 | info!("server error: {}", e); 667 | } 668 | } 669 | 670 | fn get_sid_and_state(body: &str, headers: &HeaderMap) -> Option<(String, u8)> { 671 | let mut sid = None; 672 | 673 | let hsid = headers.get("SID"); 674 | if let Some(h) = hsid { 675 | if let Ok(f) = h.to_str() { 676 | sid = Some(f.to_string()); 677 | } 678 | } 679 | 680 | let state = xml::get_binary_state(body); 681 | 682 | if sid != None && state != None { 683 | return Some((sid.unwrap(), state.unwrap())); 684 | } 685 | None 686 | } 687 | 688 | fn port_is_available(port: u16) -> bool { 689 | TcpListener::bind(("127.0.0.1", port)).is_ok() 690 | } 691 | 692 | fn get_available_port() -> Option { 693 | (8000..9000).find(|port| Self::port_is_available(*port)) 694 | } 695 | 696 | // Bind through a connected interface 697 | fn get_bind_addr() -> Result { 698 | let any: SocketAddr = ([0, 0, 0, 0], 0).into(); 699 | let dns: SocketAddr = ([1, 1, 1, 1], 80).into(); 700 | let socket = UdpSocket::bind(any)?; 701 | socket.connect(dns)?; 702 | let bind_addr = socket.local_addr()?; 703 | 704 | Ok(bind_addr) 705 | } 706 | 707 | fn parse_ssdp_response(response: &str) -> Option { 708 | for line in response.lines() { 709 | let line = line.to_lowercase(); 710 | if line.contains("location") { 711 | if let Some(idx) = line.find("http") { 712 | let url = &line[idx..line.len()]; 713 | return Some(url.trim().to_string()); 714 | } 715 | } 716 | } 717 | None 718 | } 719 | 720 | fn send_sddp_request(socket: &UdpSocket, mx: Duration) { 721 | let msg = format!( 722 | "M-SEARCH * HTTP/1.1\r 723 | Host: 239.255.255.250:1900\r 724 | Man: \"ssdp:discover\"\r 725 | ST: upnp:rootdevice\r 726 | MX: {}\r 727 | Content-Length: 0\r\n\r\n", 728 | mx.as_secs() 729 | ); 730 | 731 | let broadcast_address: SocketAddr = ([239, 255, 255, 250], 1900).into(); 732 | let _ = socket.set_broadcast(true); 733 | socket.send_to(msg.as_bytes(), &broadcast_address).unwrap(); 734 | } 735 | } 736 | -------------------------------------------------------------------------------- /examples/weeapp.rs: -------------------------------------------------------------------------------- 1 | // On Windows don't create terminal window when opening app in release mode GUI 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | extern crate weectrl; 5 | 6 | use fltk::app::MouseButton; 7 | use tracing::info; 8 | 9 | use weectrl::{DeviceInfo, DiscoveryMode, State, StateNotification, WeeController}; 10 | 11 | use fltk::{enums::Color, enums::Event, image::PngImage, image::SvgImage, prelude::*, *}; 12 | use fltk_theme::widget_schemes::fluent::colors::*; 13 | use fltk_theme::{SchemeType, WidgetScheme}; 14 | use std::collections::HashMap; 15 | use std::sync::mpsc::TryRecvError; 16 | use std::time::Duration; 17 | 18 | extern crate directories; 19 | use directories::ProjectDirs; 20 | 21 | use serde::{Deserialize, Serialize}; 22 | 23 | use std::fs::File; 24 | use std::io::prelude::*; 25 | 26 | #[derive(Debug, Clone)] 27 | enum Message { 28 | Reload, 29 | Clear, 30 | StartDiscovery, 31 | PollDiscovery, 32 | EndDiscovery, 33 | AddButton(DeviceInfo), 34 | Notification(StateNotification), 35 | Clicked(DeviceInfo), 36 | } 37 | 38 | struct WeeApp { 39 | app: app::App, 40 | scroll: group::Scroll, 41 | pack: group::Pack, 42 | reloading_frame: frame::Frame, 43 | progress_frame: frame::Frame, 44 | sender: app::Sender, 45 | receiver: app::Receiver, 46 | controller: WeeController, 47 | discovering: bool, // Is discover currently in progress? 48 | buttons: HashMap, // List of deviceID's and indexes of the associated buttons 49 | } 50 | 51 | const SETTINGS_FILE: &str = "Settings.json"; 52 | 53 | const SUBSCRIPTION_DURATION: Duration = Duration::from_secs(180); 54 | const SUBSCRIPTION_AUTO_RENEW: bool = true; 55 | const DISCOVERY_MX: Duration = Duration::from_secs(5); 56 | const CONN_TIMEOUT: Duration = Duration::from_secs(5); 57 | 58 | const UNIT_SPACING: i32 = 40; 59 | const BUTTON_HEIGHT: i32 = UNIT_SPACING; 60 | const WINDOW_WIDTH: i32 = 350; 61 | const TOP_BAR_HEIGHT: i32 = UNIT_SPACING + 10; 62 | const LIST_HEIGHT: i32 = 170; 63 | const WINDOW_HEIGHT: i32 = LIST_HEIGHT + TOP_BAR_HEIGHT + UNIT_SPACING; 64 | const SCROLL_WIDTH: i32 = 15; 65 | 66 | const BUTTON_ON_COLOR: Color = Color::from_rgb(114, 159, 207); 67 | const BUTTON_OFF_COLOR: Color = Color::from_rgb(13, 25, 38); 68 | 69 | const WINDOW_ICON: &[u8] = include_bytes!("images/titlebar.png"); 70 | 71 | const PROGRESS: &str = include_str!("images/progress.svg"); 72 | 73 | const RL_BTN1: &str = include_str!("images/refresh.svg"); 74 | const RL_BTN2: &str = include_str!("images/refresh-press.svg"); 75 | const RL_BTN3: &str = include_str!("images/refresh-hover.svg"); 76 | 77 | const CL_BTN1: &str = include_str!("images/clear.svg"); 78 | const CL_BTN2: &str = include_str!("images/clear-press.svg"); 79 | const CL_BTN3: &str = include_str!("images/clear-hover.svg"); 80 | 81 | const CLEAR_TOOLTIP: &str = "Forget all devices and clear the on-disk list of known devices."; 82 | const RELOAD_TOOLTIP: &str = 83 | "Reload list of devices from on-disk list (if any) and then by network query."; 84 | const SWITCH_TOOLTIP: &str = "Left click to toggle switch. Right click for additional information."; 85 | 86 | macro_rules! read_image { 87 | ($data:ident) => {{ 88 | let mut img = SvgImage::from_data($data).unwrap(); 89 | img.scale(30, 30, true, true); 90 | img 91 | }}; 92 | } 93 | 94 | // Builder for the information dialogue. 95 | macro_rules! build_field { 96 | ($parent:ident, $name:expr, $string:expr) => { 97 | let st = format!("{}: {:}", $name, $string); 98 | let mut name = Box::new(output::Output::default().with_size(0, 20)); 99 | name.set_value(&st); 100 | name.set_color(Color::from_hex(0x2e3436)); 101 | name.set_label_font(enums::Font::Helvetica); 102 | name.set_frame(enums::FrameType::FlatBox); 103 | name.set_text_size(16); 104 | $parent.add(&*name); 105 | }; 106 | } 107 | 108 | impl WeeApp { 109 | pub fn new() -> Self { 110 | let app = app::App::default(); 111 | app::background(0, 0, 0); 112 | app::background2(0x00, 0x00, 0x00); 113 | app::foreground(0xff, 0xff, 0xff); 114 | app::set_color( 115 | Color::Selection, 116 | SELECTION_COLOR.0, 117 | SELECTION_COLOR.1, 118 | SELECTION_COLOR.2, 119 | ); 120 | 121 | let theme = WidgetScheme::new(SchemeType::Fluent); 122 | theme.apply(); 123 | 124 | app::set_font_size(18); 125 | 126 | let (sender, receiver) = app::channel(); 127 | 128 | // get version number from Cargp.toml 129 | let version = env!("CARGO_PKG_VERSION"); 130 | 131 | // Create main Application window. Double buffered. 132 | let mut main_win = 133 | window::DoubleWindow::default().with_label(&format!("WeeApp {}", version)); 134 | 135 | let args: Vec = std::env::args().collect(); 136 | 137 | // Option -r resets window to default size 138 | let storage = Storage::new(); 139 | if args.len() == 2 && args[1] == "-r" { 140 | storage.clear(); 141 | } 142 | 143 | // Set app size/position to saved values, or use defaults 144 | // If no position is set the OS decides. 145 | let settings = storage.read(); 146 | if let Some(settings) = settings { 147 | main_win.set_size(settings.w, settings.h); 148 | main_win.set_pos(settings.x, settings.y); 149 | 150 | } else { 151 | main_win.set_size(WINDOW_WIDTH, WINDOW_HEIGHT); 152 | } 153 | 154 | main_win.set_color(Color::Gray0); 155 | 156 | main_win.set_icon(Some(PngImage::from_data(WINDOW_ICON).unwrap())); 157 | 158 | // Load all the images for clear/reload buttons 159 | let image_clear = read_image!(CL_BTN1); 160 | let image_clear_click = read_image!(CL_BTN2); 161 | let image_clear_hover = read_image!(CL_BTN3); 162 | 163 | let mut btn_clear = button::Button::default().with_size(50, 50); 164 | btn_clear.set_frame(enums::FrameType::FlatBox); 165 | btn_clear.set_pos(main_win.w() - UNIT_SPACING - 10 - UNIT_SPACING - 10, 0); 166 | btn_clear.set_image(Some(image_clear.clone())); 167 | 168 | let ic = image_clear.clone(); 169 | let icc = image_clear_click.clone(); 170 | let ich = image_clear_hover.clone(); 171 | btn_clear.emit(sender.clone(), Message::Clear); 172 | btn_clear.set_tooltip(CLEAR_TOOLTIP); 173 | 174 | btn_clear.handle(move |b, e| match e { 175 | Event::Enter | Event::Released => { 176 | b.set_image(Some(ich.clone())); 177 | b.redraw(); 178 | true 179 | } 180 | Event::Leave => { 181 | b.set_image(Some(ic.clone())); 182 | b.redraw(); 183 | true 184 | } 185 | Event::Push => { 186 | b.set_image(Some(icc.clone())); 187 | true 188 | } 189 | _ => false, 190 | }); 191 | 192 | let image_reload = read_image!(RL_BTN1); 193 | let image_reload_click = read_image!(RL_BTN2); 194 | let image_reload_hover = read_image!(RL_BTN3); 195 | 196 | let mut btn_reload = button::Button::default().with_size(50, 50); 197 | btn_reload.set_frame(enums::FrameType::FlatBox); 198 | btn_reload.set_pos(main_win.w() - UNIT_SPACING - 10, 0); 199 | btn_reload.set_image(Some(image_reload.clone())); 200 | 201 | let ir = image_reload.clone(); 202 | let irc = image_reload_click.clone(); 203 | let irh = image_reload_hover.clone(); 204 | btn_reload.set_tooltip(RELOAD_TOOLTIP); 205 | btn_reload.emit(sender.clone(), Message::Reload); 206 | 207 | btn_reload.handle(move |b, e| match e { 208 | Event::Enter | Event::Released => { 209 | b.set_image(Some(irh.clone())); 210 | b.redraw(); 211 | true 212 | } 213 | Event::Leave => { 214 | b.set_image(Some(ir.clone())); 215 | b.redraw(); 216 | true 217 | } 218 | Event::Push => { 219 | b.set_image(Some(irc.clone())); 220 | true 221 | } 222 | _ => false, 223 | }); 224 | 225 | let mut scroll = group::Scroll::new( 226 | UNIT_SPACING, 227 | TOP_BAR_HEIGHT, 228 | main_win.w() - 2 * UNIT_SPACING, 229 | main_win.h() - 2 * UNIT_SPACING - 5, 230 | "", 231 | ); 232 | 233 | scroll.set_frame(enums::FrameType::BorderBox); 234 | scroll.set_type(group::ScrollType::VerticalAlways); 235 | scroll.make_resizable(false); 236 | scroll.set_color(Color::from_hex(0x2e3436)); 237 | scroll.set_scrollbar_size(SCROLL_WIDTH); 238 | 239 | let mut pack = group::Pack::default() 240 | .with_size(scroll.w() - SCROLL_WIDTH, scroll.h()) 241 | .center_of(&scroll); 242 | 243 | pack.set_type(group::PackType::Vertical); 244 | pack.set_spacing(2); 245 | pack.set_color(Color::Red); 246 | 247 | pack.end(); 248 | 249 | main_win.end(); 250 | 251 | // The part that says "Searching" when looking for new switches on the LAN 252 | let mut reloading_frame = frame::Frame::new( 253 | UNIT_SPACING, 254 | main_win.h() - UNIT_SPACING, 255 | 90, 256 | UNIT_SPACING, 257 | "Searching", 258 | ); 259 | 260 | reloading_frame.set_label_color(Color::Yellow); 261 | main_win.add(&reloading_frame); 262 | 263 | // The rotating imagee when searching on the LAN 264 | let mut progress_frame = frame::Frame::default().with_size(16, 16); 265 | progress_frame.set_pos(90 + UNIT_SPACING + 5, main_win.h() - 27); 266 | 267 | main_win.add(&progress_frame); 268 | 269 | let mut sc = scroll.clone(); 270 | let mut pa = pack.clone(); 271 | let mut reload = btn_reload.clone(); 272 | let mut clear = btn_clear.clone(); 273 | let mut rlfr = reloading_frame.clone(); 274 | let mut prfr = progress_frame.clone(); 275 | let controller = WeeController::new(); 276 | 277 | main_win.handle(move |w, ev| match ev { 278 | // When quitting the App save Windows size/position 279 | Event::Hide => { 280 | let settings = Settings { 281 | x: w.x(), 282 | y: w.y(), 283 | w: w.w(), 284 | h: w.h(), 285 | }; 286 | let storage = Storage::new(); 287 | storage.write(settings); 288 | true 289 | } 290 | 291 | Event::Show => { 292 | info!("Event::Show"); 293 | 294 | //Sometimes the buttons would mysteriously be double height 295 | //Trying this hack to mitigate 296 | for i in 0..pa.children() { 297 | let mut btn = pa.child(i).unwrap(); 298 | btn.set_size(pa.w(), BUTTON_HEIGHT); 299 | } 300 | 301 | true 302 | } 303 | 304 | // When resizing the App window, reposition internal elements accordingly 305 | Event::Resize => { 306 | info!("Event::Resize"); 307 | 308 | sc.resize( 309 | UNIT_SPACING, 310 | TOP_BAR_HEIGHT, 311 | w.w() - 2 * UNIT_SPACING, 312 | w.h() - 2 * UNIT_SPACING - 5, 313 | ); 314 | 315 | pa.resize(sc.x(), sc.y(), sc.w() - SCROLL_WIDTH, sc.h()); 316 | 317 | reload.set_size(50, 50); 318 | reload.set_pos(w.w() - UNIT_SPACING - 10, 0); 319 | 320 | clear.set_size(50, 50); 321 | clear.set_pos(w.w() - UNIT_SPACING - 10 - UNIT_SPACING - 10, 0); 322 | 323 | rlfr.set_size(90, UNIT_SPACING); 324 | rlfr.set_pos(UNIT_SPACING, w.h() - UNIT_SPACING); 325 | 326 | prfr.set_size(16, 16); 327 | prfr.set_pos(90 + UNIT_SPACING + 5, w.h() - 27); 328 | 329 | //Sometimes the buttons would mysteriously be the wrong height 330 | //Trying this hack to mitigate 331 | for i in 0..pa.children() { 332 | let mut btn = pa.child(i).unwrap(); 333 | btn.set_size(pa.w(), BUTTON_HEIGHT); 334 | } 335 | true 336 | } 337 | _ => false, 338 | }); 339 | 340 | main_win.make_resizable(true); 341 | 342 | // Create thread to receive notifications from switches that have changed state 343 | // It will forward the messages to the UI message loop so it can update the button color 344 | let rx = controller.notify(); 345 | let sc = sender.clone(); 346 | let _ignore = std::thread::Builder::new() 347 | .name("APP_notifiy".to_string()) 348 | .spawn(move || { 349 | while let Ok(notification) = rx.recv() { 350 | sc.send(Message::Notification(notification)); 351 | } 352 | }) 353 | .unwrap(); 354 | 355 | main_win.show(); 356 | 357 | // Start looking for switches, through the UI message loop 358 | sender.send(Message::StartDiscovery); 359 | 360 | Self { 361 | app, 362 | pack, 363 | scroll, 364 | reloading_frame, 365 | progress_frame, 366 | sender, 367 | receiver, 368 | controller, 369 | discovering: true, 370 | buttons: HashMap::new(), 371 | } 372 | } 373 | 374 | fn show_popup(device: &DeviceInfo, icons: Option>) { 375 | const WIND_WIDTH: i32 = 500; 376 | const WIND_HEIGHT: i32 = 500; 377 | const PADDING: i32 = 15; 378 | const TAB_WIDTH: i32 = WIND_WIDTH - 2 * PADDING; 379 | const TAB_HEIGHT: i32 = WIND_WIDTH - 2 * PADDING; 380 | const GROUP_HEIGHT: i32 = TAB_HEIGHT - 25; 381 | 382 | let mut window = window::Window::default().with_label(&device.friendly_name); 383 | window.set_size(WIND_WIDTH, WIND_HEIGHT); 384 | window.set_icon(Some(PngImage::from_data(WINDOW_ICON).unwrap())); 385 | 386 | let tab = group::Tabs::new(PADDING, PADDING, TAB_WIDTH, TAB_HEIGHT, ""); 387 | 388 | let grp1 = group::Group::new(PADDING, PADDING + 25, TAB_WIDTH, GROUP_HEIGHT, "Info\t\t"); 389 | 390 | let mut pack = group::Pack::default() 391 | .with_pos(25, 80) 392 | .with_size(grp1.w() - 25, grp1.h() - 150); 393 | 394 | pack.set_type(group::PackType::Vertical); 395 | pack.set_spacing(2); 396 | pack.set_color(Color::Red); 397 | pack.set_spacing(5); 398 | 399 | pack.end(); 400 | grp1.end(); 401 | 402 | if let Some(icons) = icons { 403 | let mut frame = frame::Frame::default().with_size(200, 200).center_of(&pack); 404 | frame.set_frame(enums::FrameType::FlatBox); 405 | frame.set_color(Color::from_hex(0x2e3436)); 406 | 407 | let icon = icons.get(0).unwrap(); 408 | 409 | if icon.mimetype == mime::IMAGE_PNG { 410 | if let Ok(img) = PngImage::from_data(icon.data.as_slice()) { 411 | frame.set_image(Some(img)); 412 | } 413 | } else if icon.mimetype == mime::IMAGE_JPEG { 414 | if let Ok(img) = image::JpegImage::from_data(icon.data.as_slice()) { 415 | frame.set_image(Some(img)); 416 | } 417 | } else if icon.mimetype == mime::IMAGE_GIF { 418 | if let Ok(img) = image::GifImage::from_data(icon.data.as_slice()) { 419 | frame.set_image(Some(img)); 420 | } 421 | } 422 | pack.add(&frame); 423 | } 424 | 425 | let spacer = frame::Frame::new(0, 0, 0, 30, " "); 426 | pack.add(&spacer); 427 | build_field!(pack, " Name", &device.friendly_name); 428 | build_field!(pack, " Model", &device.model); 429 | build_field!(pack, " Hostname", &device.hostname); 430 | build_field!(pack, " Location", &device.location); 431 | 432 | use advmac::{MacAddr6, MacAddrFormat}; 433 | 434 | if let Ok(mac) = MacAddr6::parse_str(&device.root.device.mac_address) { 435 | let str = mac.format_string(MacAddrFormat::ColonNotation); 436 | build_field!(pack, " MAC Address", &str); 437 | } 438 | 439 | // TAB 2 440 | let grp2 = group::Group::new(PADDING, PADDING + 25, TAB_WIDTH, GROUP_HEIGHT, "Homepage\t"); 441 | 442 | let mut scroll = group::Scroll::new(20, 50, grp2.w() - 20, grp2.h() - 40, ""); 443 | 444 | scroll.set_frame(enums::FrameType::BorderBox); 445 | scroll.set_type(group::ScrollType::BothAlways); 446 | scroll.make_resizable(false); 447 | scroll.set_color(Color::from_hex(0x2e3436)); 448 | scroll.set_scrollbar_size(SCROLL_WIDTH); 449 | 450 | let mut name = output::MultilineOutput::new(10, 35, grp2.w() + 100, grp2.h() * 5, ""); 451 | name.set_value(&device.xml); 452 | name.set_color(Color::from_hex(0x2e3436)); 453 | name.set_label_font(enums::Font::CourierBold); 454 | name.set_frame(enums::FrameType::FlatBox); 455 | name.set_text_size(14); 456 | 457 | scroll.end(); 458 | scroll.scroll_to(0, 0); 459 | scroll.redraw(); 460 | 461 | grp2.end(); 462 | tab.end(); 463 | 464 | window.end(); 465 | 466 | window.show(); 467 | } 468 | 469 | pub fn run(mut self) { 470 | let mut disc_mpsc: Option> = None; 471 | let mut timeout_handle: Option = None; 472 | let mut animation = AnimatedSvg::new(PROGRESS); 473 | while self.app.wait() { 474 | if let Some(msg) = self.receiver.recv() { 475 | match msg { 476 | // Clear button pressed, forget all switches and clear the UI 477 | Message::Clear => { 478 | if !self.discovering { 479 | info!("Message::Clear"); 480 | self.controller.clear(true); 481 | self.buttons.clear(); 482 | self.pack.clear(); 483 | self.scroll.redraw(); 484 | } 485 | } 486 | 487 | // Reload button pressed, query WeeCtrl for devices in Cache and on LAN 488 | Message::Reload => { 489 | if !self.discovering { 490 | info!("Message::Reload"); 491 | self.controller.clear(false); 492 | self.buttons.clear(); 493 | self.pack.clear(); 494 | self.scroll.redraw(); 495 | self.sender.send(Message::StartDiscovery); 496 | } 497 | } 498 | 499 | // WeeCtrl have found a switch, create a button for it 500 | Message::AddButton(device) => { 501 | info!( 502 | "Message::AddButton {:?} {:?}", 503 | device.unique_id, device.friendly_name 504 | ); 505 | let _ignore = self.controller.subscribe( 506 | &device.unique_id, 507 | SUBSCRIPTION_DURATION, 508 | SUBSCRIPTION_AUTO_RENEW, 509 | CONN_TIMEOUT, 510 | ); 511 | 512 | let mut but = button::Button::default() 513 | .with_label(&format!("{}", device.friendly_name)); 514 | 515 | but.set_size(self.pack.w(), BUTTON_HEIGHT); 516 | but.set_tooltip(SWITCH_TOOLTIP); 517 | 518 | if device.state == State::On { 519 | but.set_color(BUTTON_ON_COLOR); 520 | } else { 521 | but.set_color(BUTTON_OFF_COLOR); 522 | } 523 | 524 | but.handle(move |b, e| match e { 525 | Event::Enter => { 526 | b.set_frame(enums::FrameType::DiamondUpBox); 527 | b.redraw(); 528 | true 529 | } 530 | Event::Leave => { 531 | b.set_frame(enums::FrameType::UpBox); 532 | b.redraw(); 533 | true 534 | } 535 | _ => false, 536 | }); 537 | 538 | self.buttons.insert(device.unique_id.clone(), but.clone()); 539 | but.emit(self.sender.clone(), Message::Clicked(device)); 540 | 541 | self.pack.add(&but); 542 | 543 | self.scroll.scroll_to(0, 0); 544 | self.app.redraw(); 545 | } 546 | 547 | // A button was clicked, flip state of the switch and update the color 548 | // of the button according to returned result if it worked. 549 | Message::Clicked(device) => { 550 | info!( 551 | "Message::Clicked MB({:?}) {:?} {:?}", 552 | app::event_mouse_button(), 553 | device.unique_id, 554 | device.friendly_name 555 | ); 556 | 557 | if let Some(btn) = self.buttons.get_mut(&device.unique_id) { 558 | if app::event_mouse_button() == MouseButton::Right { 559 | let mut icons: Option> = None; 560 | if let Ok(res) = 561 | self.controller.get_icons(&device.unique_id, CONN_TIMEOUT) 562 | { 563 | if res.len() > 0 { 564 | icons = Some(res); 565 | } 566 | } 567 | Self::show_popup(&device, icons); 568 | } else { 569 | let state = if btn.color() == BUTTON_ON_COLOR { 570 | State::Off 571 | } else { 572 | State::On 573 | }; 574 | 575 | if let Ok(ret_state) = self.controller.set_binary_state( 576 | &device.unique_id, 577 | state, 578 | CONN_TIMEOUT, 579 | ) { 580 | if ret_state == State::On { 581 | btn.set_color(BUTTON_ON_COLOR); 582 | } else { 583 | btn.set_color(BUTTON_OFF_COLOR); 584 | } 585 | } 586 | } 587 | } 588 | } 589 | 590 | // Display "Searching..." message and setup a thread to receive 591 | // switches found, the thread will forward them to the UI message loop 592 | // as Message::AddButton(Device) and end after mx seconds when the channel is closed. 593 | Message::StartDiscovery => { 594 | info!("Message::StartDiscovery"); 595 | self.discovering = true; 596 | self.reloading_frame.set_label("Searching"); 597 | self.reloading_frame.show(); 598 | 599 | disc_mpsc = Some(self.controller.discover( 600 | DiscoveryMode::CacheAndBroadcast, 601 | false, 602 | DISCOVERY_MX, 603 | )); 604 | 605 | // Start polling mechanism and UI spinner 606 | let s = self.sender.clone(); 607 | timeout_handle = Some(app::add_timeout3(0.05, move |_| { 608 | s.send(Message::PollDiscovery) 609 | })); 610 | } 611 | 612 | // Since we animate the spinner by rotating it at 30fps we might as well 613 | // just poll the discovery future mpsc here. 614 | // When the future returns None we can move to EndDiscovery. 615 | Message::PollDiscovery => { 616 | //info!("Message::PollDiscovery"); 617 | if let Some(d) = disc_mpsc.as_mut() { 618 | // Animate spinner 619 | animation.rotate(4); 620 | let mut img = animation.to_svg_image(); 621 | img.scale(21, 21, true, true); 622 | self.progress_frame.set_image(Some(img)); 623 | self.progress_frame.hide(); 624 | self.progress_frame.show(); 625 | self.progress_frame.redraw(); 626 | 627 | // poll again in 33 ms 628 | if let Some(h) = timeout_handle { 629 | app::repeat_timeout3(0.033, h); 630 | } 631 | 632 | // Check channel for news or disconnection(discovery finished) 633 | match d.try_recv() { 634 | Ok(d) => self.sender.send(Message::AddButton(d)), 635 | Err(TryRecvError::Disconnected) => { 636 | info!("Ending discovery"); 637 | if let Some(h) = timeout_handle { 638 | app::remove_timeout3(h); 639 | } 640 | self.sender.send(Message::EndDiscovery); 641 | } 642 | Err(TryRecvError::Empty) => (), 643 | }; 644 | } 645 | } 646 | 647 | // Discovery phase ended, update UI accordingly. 648 | Message::EndDiscovery => { 649 | info!("Message::EndDiscovery"); 650 | self.discovering = false; 651 | self.progress_frame.hide(); 652 | self.reloading_frame.hide(); 653 | } 654 | 655 | // A switch have changed state, update the UI accordingly. 656 | Message::Notification(n) => { 657 | info!("Message::Notification: {:?} {:?}", n.unique_id, n.state); 658 | 659 | if let Some(btn) = self.buttons.get_mut(&n.unique_id) { 660 | if n.state == State::On { 661 | btn.set_color(BUTTON_ON_COLOR); 662 | } else { 663 | btn.set_color(BUTTON_OFF_COLOR); 664 | } 665 | 666 | self.app.redraw(); 667 | } 668 | } 669 | } 670 | } 671 | } 672 | } 673 | } 674 | 675 | struct AnimatedSvg { 676 | // Svg file with a single rotate(000,x,y) instruction 677 | svg: String, 678 | // Current degrees of rotation 679 | position: u32, 680 | // Location of the hardcoded "000" in "rotate(000" to modify 681 | range: core::ops::Range, 682 | } 683 | 684 | impl AnimatedSvg { 685 | pub fn new(data: &str) -> Self { 686 | let location = data.find("rotate(000").unwrap() + 7; 687 | AnimatedSvg { 688 | svg: data.to_string(), 689 | position: 0, 690 | range: location..location + 3, 691 | } 692 | } 693 | 694 | // Rotate by current position + degrees delta. 695 | // Rotates the svg by modifying the svg data in place 696 | // much faster than string replace but unsafe 697 | // the constructor would have panicked at find(..).unwrap() if it wasn't possible 698 | pub fn rotate(&mut self, degrees: u32) { 699 | self.position += degrees; 700 | if self.position > 360 { 701 | self.position = 0 702 | } 703 | let do_rotate = format!("{:03}", self.position); 704 | let range = self.range.clone(); 705 | unsafe { 706 | core::ptr::copy_nonoverlapping(do_rotate.as_ptr(), self.svg[range].as_mut_ptr(), 3); 707 | } 708 | } 709 | 710 | fn to_svg_image(&self) -> SvgImage { 711 | SvgImage::from_data(&self.svg).unwrap() 712 | } 713 | } 714 | 715 | #[derive(Clone, Debug, Serialize, Deserialize)] 716 | pub struct Settings { 717 | x: i32, 718 | y: i32, 719 | w: i32, 720 | h: i32, 721 | } 722 | 723 | pub struct Storage { 724 | cache_file: Option, 725 | } 726 | 727 | impl Default for Storage { 728 | fn default() -> Self { 729 | Self::new() 730 | } 731 | } 732 | 733 | impl Storage { 734 | #[must_use] 735 | pub fn new() -> Self { 736 | let mut file_path: Option = None; 737 | 738 | if let Some(proj_dirs) = ProjectDirs::from("", "", "WeeApp") { 739 | let mut path = proj_dirs.config_dir().to_path_buf(); 740 | path.push(SETTINGS_FILE); 741 | file_path = Some(path); 742 | info!("Settings file: {:#?}", file_path); 743 | } 744 | Self { 745 | cache_file: file_path, 746 | } 747 | } 748 | 749 | /// Write data to cache, errors ignored 750 | pub fn write(&self, settings: Settings) { 751 | info!("Saving settings: {:#?}", settings); 752 | 753 | if let Some(ref fpath) = self.cache_file { 754 | let data = settings; 755 | if let Some(prefix) = fpath.parent() { 756 | let _ignore = std::fs::create_dir_all(prefix); 757 | 758 | if let Ok(serialized) = serde_json::to_string(&data) { 759 | if let Ok(mut buffer) = File::create(fpath) { 760 | let _ignore = buffer.write_all(&serialized.into_bytes()); 761 | } 762 | } 763 | } 764 | } 765 | } 766 | 767 | pub fn read(&self) -> Option { 768 | if let Some(ref fpath) = self.cache_file { 769 | if let Ok(mut file) = File::open(fpath) { 770 | let mut s = String::new(); 771 | let _ignore = file.read_to_string(&mut s); 772 | let data: Option = serde_json::from_str(&s).ok(); 773 | info!("read settings: {:#?}", data); 774 | return data; 775 | } 776 | } 777 | None 778 | } 779 | 780 | pub fn clear(&self) { 781 | if let Some(ref fpath) = self.cache_file { 782 | let _ignore = std::fs::remove_file(fpath); 783 | } 784 | } 785 | } 786 | 787 | fn main() { 788 | use tracing_subscriber::fmt::time; 789 | 790 | tracing_subscriber::fmt() 791 | .with_timer(time::LocalTime::rfc_3339()) 792 | .init(); 793 | 794 | WeeApp::new().run(); 795 | } 796 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "advmac" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8323b66cda4fced4580b2e9f39882f3bbfee49a78e7308b426bd15b58eb2ba28" 10 | dependencies = [ 11 | "arrayvec", 12 | "rand", 13 | "serde", 14 | ] 15 | 16 | [[package]] 17 | name = "ansi_term" 18 | version = "0.12.1" 19 | source = "registry+https://github.com/rust-lang/crates.io-index" 20 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 21 | dependencies = [ 22 | "winapi", 23 | ] 24 | 25 | [[package]] 26 | name = "arrayvec" 27 | version = "0.7.2" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" 30 | dependencies = [ 31 | "serde", 32 | ] 33 | 34 | [[package]] 35 | name = "autocfg" 36 | version = "1.1.0" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 39 | 40 | [[package]] 41 | name = "base64" 42 | version = "0.13.0" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 45 | 46 | [[package]] 47 | name = "bitflags" 48 | version = "1.3.2" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 51 | 52 | [[package]] 53 | name = "bitflags" 54 | version = "2.4.1" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" 57 | 58 | [[package]] 59 | name = "bumpalo" 60 | version = "3.11.0" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" 63 | 64 | [[package]] 65 | name = "bytes" 66 | version = "1.2.1" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" 69 | 70 | [[package]] 71 | name = "cc" 72 | version = "1.2.26" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" 75 | dependencies = [ 76 | "shlex", 77 | ] 78 | 79 | [[package]] 80 | name = "cfg-if" 81 | version = "1.0.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 84 | 85 | [[package]] 86 | name = "cmake" 87 | version = "0.1.54" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" 90 | dependencies = [ 91 | "cc", 92 | ] 93 | 94 | [[package]] 95 | name = "cmk" 96 | version = "0.1.2" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "8fd5de2a10e31b3ec3e8d75e7ccf8281ab3ee55de68f7ab6ffa9e21be8d82f22" 99 | 100 | [[package]] 101 | name = "core-foundation" 102 | version = "0.9.3" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 105 | dependencies = [ 106 | "core-foundation-sys", 107 | "libc", 108 | ] 109 | 110 | [[package]] 111 | name = "core-foundation-sys" 112 | version = "0.8.3" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 115 | 116 | [[package]] 117 | name = "crossbeam-channel" 118 | version = "0.5.6" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" 121 | dependencies = [ 122 | "cfg-if", 123 | "crossbeam-utils", 124 | ] 125 | 126 | [[package]] 127 | name = "crossbeam-utils" 128 | version = "0.8.11" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" 131 | dependencies = [ 132 | "cfg-if", 133 | "once_cell", 134 | ] 135 | 136 | [[package]] 137 | name = "directories" 138 | version = "5.0.1" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" 141 | dependencies = [ 142 | "dirs-sys", 143 | ] 144 | 145 | [[package]] 146 | name = "dirs-sys" 147 | version = "0.4.1" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 150 | dependencies = [ 151 | "libc", 152 | "option-ext", 153 | "redox_users", 154 | "windows-sys 0.48.0", 155 | ] 156 | 157 | [[package]] 158 | name = "encoding_rs" 159 | version = "0.8.31" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" 162 | dependencies = [ 163 | "cfg-if", 164 | ] 165 | 166 | [[package]] 167 | name = "fastrand" 168 | version = "1.8.0" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" 171 | dependencies = [ 172 | "instant", 173 | ] 174 | 175 | [[package]] 176 | name = "fltk" 177 | version = "1.5.9" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "ecfe1d97c0be7585b46d52c1e563226a6495a993be2c72dcdd03fe79d29b1f2a" 180 | dependencies = [ 181 | "bitflags 2.4.1", 182 | "crossbeam-channel", 183 | "fltk-sys", 184 | "once_cell", 185 | "paste", 186 | "ttf-parser", 187 | ] 188 | 189 | [[package]] 190 | name = "fltk-sys" 191 | version = "1.5.9" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "de39f27a66d3ceefca611ee2db890369c6846fde4de7b5fd7cdedd9ce0c432f8" 194 | dependencies = [ 195 | "cmake", 196 | "cmk", 197 | ] 198 | 199 | [[package]] 200 | name = "fltk-theme" 201 | version = "0.7.2" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "3539d27a815514b56af2afa6b8e7c6d6b9274a103239487d5a60daa6340a4868" 204 | dependencies = [ 205 | "fltk", 206 | ] 207 | 208 | [[package]] 209 | name = "fnv" 210 | version = "1.0.7" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 213 | 214 | [[package]] 215 | name = "foreign-types" 216 | version = "0.3.2" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 219 | dependencies = [ 220 | "foreign-types-shared", 221 | ] 222 | 223 | [[package]] 224 | name = "foreign-types-shared" 225 | version = "0.1.1" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 228 | 229 | [[package]] 230 | name = "form_urlencoded" 231 | version = "1.1.0" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" 234 | dependencies = [ 235 | "percent-encoding", 236 | ] 237 | 238 | [[package]] 239 | name = "futures" 240 | version = "0.3.24" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c" 243 | dependencies = [ 244 | "futures-channel", 245 | "futures-core", 246 | "futures-executor", 247 | "futures-io", 248 | "futures-sink", 249 | "futures-task", 250 | "futures-util", 251 | ] 252 | 253 | [[package]] 254 | name = "futures-channel" 255 | version = "0.3.24" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050" 258 | dependencies = [ 259 | "futures-core", 260 | "futures-sink", 261 | ] 262 | 263 | [[package]] 264 | name = "futures-core" 265 | version = "0.3.24" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" 268 | 269 | [[package]] 270 | name = "futures-executor" 271 | version = "0.3.24" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab" 274 | dependencies = [ 275 | "futures-core", 276 | "futures-task", 277 | "futures-util", 278 | ] 279 | 280 | [[package]] 281 | name = "futures-io" 282 | version = "0.3.24" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" 285 | 286 | [[package]] 287 | name = "futures-macro" 288 | version = "0.3.24" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17" 291 | dependencies = [ 292 | "proc-macro2", 293 | "quote", 294 | "syn", 295 | ] 296 | 297 | [[package]] 298 | name = "futures-sink" 299 | version = "0.3.24" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56" 302 | 303 | [[package]] 304 | name = "futures-task" 305 | version = "0.3.24" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" 308 | 309 | [[package]] 310 | name = "futures-util" 311 | version = "0.3.24" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" 314 | dependencies = [ 315 | "futures-channel", 316 | "futures-core", 317 | "futures-io", 318 | "futures-macro", 319 | "futures-sink", 320 | "futures-task", 321 | "memchr", 322 | "pin-project-lite", 323 | "pin-utils", 324 | "slab", 325 | ] 326 | 327 | [[package]] 328 | name = "getrandom" 329 | version = "0.2.7" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" 332 | dependencies = [ 333 | "cfg-if", 334 | "libc", 335 | "wasi", 336 | ] 337 | 338 | [[package]] 339 | name = "h2" 340 | version = "0.3.14" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be" 343 | dependencies = [ 344 | "bytes", 345 | "fnv", 346 | "futures-core", 347 | "futures-sink", 348 | "futures-util", 349 | "http", 350 | "indexmap", 351 | "slab", 352 | "tokio", 353 | "tokio-util", 354 | "tracing", 355 | ] 356 | 357 | [[package]] 358 | name = "hashbrown" 359 | version = "0.12.3" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 362 | 363 | [[package]] 364 | name = "hermit-abi" 365 | version = "0.1.19" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 368 | dependencies = [ 369 | "libc", 370 | ] 371 | 372 | [[package]] 373 | name = "http" 374 | version = "0.2.8" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" 377 | dependencies = [ 378 | "bytes", 379 | "fnv", 380 | "itoa", 381 | ] 382 | 383 | [[package]] 384 | name = "http-body" 385 | version = "0.4.5" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" 388 | dependencies = [ 389 | "bytes", 390 | "http", 391 | "pin-project-lite", 392 | ] 393 | 394 | [[package]] 395 | name = "httparse" 396 | version = "1.8.0" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 399 | 400 | [[package]] 401 | name = "httpdate" 402 | version = "1.0.2" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" 405 | 406 | [[package]] 407 | name = "hyper" 408 | version = "0.14.20" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" 411 | dependencies = [ 412 | "bytes", 413 | "futures-channel", 414 | "futures-core", 415 | "futures-util", 416 | "h2", 417 | "http", 418 | "http-body", 419 | "httparse", 420 | "httpdate", 421 | "itoa", 422 | "pin-project-lite", 423 | "socket2", 424 | "tokio", 425 | "tower-service", 426 | "tracing", 427 | "want", 428 | ] 429 | 430 | [[package]] 431 | name = "hyper-tls" 432 | version = "0.5.0" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" 435 | dependencies = [ 436 | "bytes", 437 | "hyper", 438 | "native-tls", 439 | "tokio", 440 | "tokio-native-tls", 441 | ] 442 | 443 | [[package]] 444 | name = "idna" 445 | version = "0.3.0" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" 448 | dependencies = [ 449 | "unicode-bidi", 450 | "unicode-normalization", 451 | ] 452 | 453 | [[package]] 454 | name = "indexmap" 455 | version = "1.9.1" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" 458 | dependencies = [ 459 | "autocfg", 460 | "hashbrown", 461 | ] 462 | 463 | [[package]] 464 | name = "instant" 465 | version = "0.1.12" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 468 | dependencies = [ 469 | "cfg-if", 470 | ] 471 | 472 | [[package]] 473 | name = "ipnet" 474 | version = "2.5.0" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" 477 | 478 | [[package]] 479 | name = "itoa" 480 | version = "1.0.3" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" 483 | 484 | [[package]] 485 | name = "js-sys" 486 | version = "0.3.60" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" 489 | dependencies = [ 490 | "wasm-bindgen", 491 | ] 492 | 493 | [[package]] 494 | name = "lazy_static" 495 | version = "1.4.0" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 498 | 499 | [[package]] 500 | name = "libc" 501 | version = "0.2.132" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" 504 | 505 | [[package]] 506 | name = "lock_api" 507 | version = "0.4.9" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" 510 | dependencies = [ 511 | "autocfg", 512 | "scopeguard", 513 | ] 514 | 515 | [[package]] 516 | name = "log" 517 | version = "0.4.17" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 520 | dependencies = [ 521 | "cfg-if", 522 | ] 523 | 524 | [[package]] 525 | name = "memchr" 526 | version = "2.5.0" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 529 | 530 | [[package]] 531 | name = "mime" 532 | version = "0.3.16" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 535 | 536 | [[package]] 537 | name = "mio" 538 | version = "0.8.4" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" 541 | dependencies = [ 542 | "libc", 543 | "log", 544 | "wasi", 545 | "windows-sys 0.36.1", 546 | ] 547 | 548 | [[package]] 549 | name = "native-tls" 550 | version = "0.2.10" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" 553 | dependencies = [ 554 | "lazy_static", 555 | "libc", 556 | "log", 557 | "openssl", 558 | "openssl-probe", 559 | "openssl-sys", 560 | "schannel", 561 | "security-framework", 562 | "security-framework-sys", 563 | "tempfile", 564 | ] 565 | 566 | [[package]] 567 | name = "num_cpus" 568 | version = "1.13.1" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" 571 | dependencies = [ 572 | "hermit-abi", 573 | "libc", 574 | ] 575 | 576 | [[package]] 577 | name = "num_threads" 578 | version = "0.1.6" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" 581 | dependencies = [ 582 | "libc", 583 | ] 584 | 585 | [[package]] 586 | name = "once_cell" 587 | version = "1.14.0" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" 590 | 591 | [[package]] 592 | name = "openssl" 593 | version = "0.10.41" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0" 596 | dependencies = [ 597 | "bitflags 1.3.2", 598 | "cfg-if", 599 | "foreign-types", 600 | "libc", 601 | "once_cell", 602 | "openssl-macros", 603 | "openssl-sys", 604 | ] 605 | 606 | [[package]] 607 | name = "openssl-macros" 608 | version = "0.1.0" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" 611 | dependencies = [ 612 | "proc-macro2", 613 | "quote", 614 | "syn", 615 | ] 616 | 617 | [[package]] 618 | name = "openssl-probe" 619 | version = "0.1.5" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 622 | 623 | [[package]] 624 | name = "openssl-sys" 625 | version = "0.9.75" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f" 628 | dependencies = [ 629 | "autocfg", 630 | "cc", 631 | "libc", 632 | "pkg-config", 633 | "vcpkg", 634 | ] 635 | 636 | [[package]] 637 | name = "option-ext" 638 | version = "0.2.0" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 641 | 642 | [[package]] 643 | name = "parking_lot" 644 | version = "0.12.1" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 647 | dependencies = [ 648 | "lock_api", 649 | "parking_lot_core", 650 | ] 651 | 652 | [[package]] 653 | name = "parking_lot_core" 654 | version = "0.9.3" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" 657 | dependencies = [ 658 | "cfg-if", 659 | "libc", 660 | "redox_syscall", 661 | "smallvec", 662 | "windows-sys 0.36.1", 663 | ] 664 | 665 | [[package]] 666 | name = "paste" 667 | version = "1.0.9" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1" 670 | 671 | [[package]] 672 | name = "percent-encoding" 673 | version = "2.2.0" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" 676 | 677 | [[package]] 678 | name = "pin-project-lite" 679 | version = "0.2.9" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 682 | 683 | [[package]] 684 | name = "pin-utils" 685 | version = "0.1.0" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 688 | 689 | [[package]] 690 | name = "pkg-config" 691 | version = "0.3.25" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" 694 | 695 | [[package]] 696 | name = "proc-macro2" 697 | version = "1.0.43" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" 700 | dependencies = [ 701 | "unicode-ident", 702 | ] 703 | 704 | [[package]] 705 | name = "quote" 706 | version = "1.0.21" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" 709 | dependencies = [ 710 | "proc-macro2", 711 | ] 712 | 713 | [[package]] 714 | name = "rand" 715 | version = "0.8.5" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 718 | dependencies = [ 719 | "rand_core", 720 | ] 721 | 722 | [[package]] 723 | name = "rand_core" 724 | version = "0.6.4" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 727 | dependencies = [ 728 | "getrandom", 729 | ] 730 | 731 | [[package]] 732 | name = "redox_syscall" 733 | version = "0.2.16" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 736 | dependencies = [ 737 | "bitflags 1.3.2", 738 | ] 739 | 740 | [[package]] 741 | name = "redox_users" 742 | version = "0.4.3" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" 745 | dependencies = [ 746 | "getrandom", 747 | "redox_syscall", 748 | "thiserror", 749 | ] 750 | 751 | [[package]] 752 | name = "remove_dir_all" 753 | version = "0.5.3" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 756 | dependencies = [ 757 | "winapi", 758 | ] 759 | 760 | [[package]] 761 | name = "reqwest" 762 | version = "0.11.11" 763 | source = "registry+https://github.com/rust-lang/crates.io-index" 764 | checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" 765 | dependencies = [ 766 | "base64", 767 | "bytes", 768 | "encoding_rs", 769 | "futures-core", 770 | "futures-util", 771 | "h2", 772 | "http", 773 | "http-body", 774 | "hyper", 775 | "hyper-tls", 776 | "ipnet", 777 | "js-sys", 778 | "lazy_static", 779 | "log", 780 | "mime", 781 | "native-tls", 782 | "percent-encoding", 783 | "pin-project-lite", 784 | "serde", 785 | "serde_json", 786 | "serde_urlencoded", 787 | "tokio", 788 | "tokio-native-tls", 789 | "tower-service", 790 | "url", 791 | "wasm-bindgen", 792 | "wasm-bindgen-futures", 793 | "web-sys", 794 | "winreg", 795 | ] 796 | 797 | [[package]] 798 | name = "ryu" 799 | version = "1.0.11" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" 802 | 803 | [[package]] 804 | name = "schannel" 805 | version = "0.1.20" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" 808 | dependencies = [ 809 | "lazy_static", 810 | "windows-sys 0.36.1", 811 | ] 812 | 813 | [[package]] 814 | name = "scopeguard" 815 | version = "1.1.0" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 818 | 819 | [[package]] 820 | name = "security-framework" 821 | version = "2.7.0" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" 824 | dependencies = [ 825 | "bitflags 1.3.2", 826 | "core-foundation", 827 | "core-foundation-sys", 828 | "libc", 829 | "security-framework-sys", 830 | ] 831 | 832 | [[package]] 833 | name = "security-framework-sys" 834 | version = "2.6.1" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" 837 | dependencies = [ 838 | "core-foundation-sys", 839 | "libc", 840 | ] 841 | 842 | [[package]] 843 | name = "serde" 844 | version = "1.0.145" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" 847 | dependencies = [ 848 | "serde_derive", 849 | ] 850 | 851 | [[package]] 852 | name = "serde-xml-rs" 853 | version = "0.6.0" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782" 856 | dependencies = [ 857 | "log", 858 | "serde", 859 | "thiserror", 860 | "xml-rs", 861 | ] 862 | 863 | [[package]] 864 | name = "serde_derive" 865 | version = "1.0.145" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" 868 | dependencies = [ 869 | "proc-macro2", 870 | "quote", 871 | "syn", 872 | ] 873 | 874 | [[package]] 875 | name = "serde_json" 876 | version = "1.0.85" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" 879 | dependencies = [ 880 | "itoa", 881 | "ryu", 882 | "serde", 883 | ] 884 | 885 | [[package]] 886 | name = "serde_urlencoded" 887 | version = "0.7.1" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 890 | dependencies = [ 891 | "form_urlencoded", 892 | "itoa", 893 | "ryu", 894 | "serde", 895 | ] 896 | 897 | [[package]] 898 | name = "sharded-slab" 899 | version = "0.1.4" 900 | source = "registry+https://github.com/rust-lang/crates.io-index" 901 | checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" 902 | dependencies = [ 903 | "lazy_static", 904 | ] 905 | 906 | [[package]] 907 | name = "shlex" 908 | version = "1.3.0" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 911 | 912 | [[package]] 913 | name = "signal-hook-registry" 914 | version = "1.4.0" 915 | source = "registry+https://github.com/rust-lang/crates.io-index" 916 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 917 | dependencies = [ 918 | "libc", 919 | ] 920 | 921 | [[package]] 922 | name = "slab" 923 | version = "0.4.7" 924 | source = "registry+https://github.com/rust-lang/crates.io-index" 925 | checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" 926 | dependencies = [ 927 | "autocfg", 928 | ] 929 | 930 | [[package]] 931 | name = "smallvec" 932 | version = "1.9.0" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" 935 | 936 | [[package]] 937 | name = "socket2" 938 | version = "0.4.7" 939 | source = "registry+https://github.com/rust-lang/crates.io-index" 940 | checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" 941 | dependencies = [ 942 | "libc", 943 | "winapi", 944 | ] 945 | 946 | [[package]] 947 | name = "syn" 948 | version = "1.0.100" 949 | source = "registry+https://github.com/rust-lang/crates.io-index" 950 | checksum = "52205623b1b0f064a4e71182c3b18ae902267282930c6d5462c91b859668426e" 951 | dependencies = [ 952 | "proc-macro2", 953 | "quote", 954 | "unicode-ident", 955 | ] 956 | 957 | [[package]] 958 | name = "tempfile" 959 | version = "3.3.0" 960 | source = "registry+https://github.com/rust-lang/crates.io-index" 961 | checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" 962 | dependencies = [ 963 | "cfg-if", 964 | "fastrand", 965 | "libc", 966 | "redox_syscall", 967 | "remove_dir_all", 968 | "winapi", 969 | ] 970 | 971 | [[package]] 972 | name = "thiserror" 973 | version = "1.0.35" 974 | source = "registry+https://github.com/rust-lang/crates.io-index" 975 | checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85" 976 | dependencies = [ 977 | "thiserror-impl", 978 | ] 979 | 980 | [[package]] 981 | name = "thiserror-impl" 982 | version = "1.0.35" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783" 985 | dependencies = [ 986 | "proc-macro2", 987 | "quote", 988 | "syn", 989 | ] 990 | 991 | [[package]] 992 | name = "thread_local" 993 | version = "1.1.4" 994 | source = "registry+https://github.com/rust-lang/crates.io-index" 995 | checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" 996 | dependencies = [ 997 | "once_cell", 998 | ] 999 | 1000 | [[package]] 1001 | name = "time" 1002 | version = "0.3.14" 1003 | source = "registry+https://github.com/rust-lang/crates.io-index" 1004 | checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" 1005 | dependencies = [ 1006 | "itoa", 1007 | "libc", 1008 | "num_threads", 1009 | ] 1010 | 1011 | [[package]] 1012 | name = "tinyvec" 1013 | version = "1.6.0" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 1016 | dependencies = [ 1017 | "tinyvec_macros", 1018 | ] 1019 | 1020 | [[package]] 1021 | name = "tinyvec_macros" 1022 | version = "0.1.0" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 1025 | 1026 | [[package]] 1027 | name = "tokio" 1028 | version = "1.21.1" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95" 1031 | dependencies = [ 1032 | "autocfg", 1033 | "bytes", 1034 | "libc", 1035 | "memchr", 1036 | "mio", 1037 | "num_cpus", 1038 | "once_cell", 1039 | "parking_lot", 1040 | "pin-project-lite", 1041 | "signal-hook-registry", 1042 | "socket2", 1043 | "tokio-macros", 1044 | "winapi", 1045 | ] 1046 | 1047 | [[package]] 1048 | name = "tokio-macros" 1049 | version = "1.8.0" 1050 | source = "registry+https://github.com/rust-lang/crates.io-index" 1051 | checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" 1052 | dependencies = [ 1053 | "proc-macro2", 1054 | "quote", 1055 | "syn", 1056 | ] 1057 | 1058 | [[package]] 1059 | name = "tokio-native-tls" 1060 | version = "0.3.0" 1061 | source = "registry+https://github.com/rust-lang/crates.io-index" 1062 | checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" 1063 | dependencies = [ 1064 | "native-tls", 1065 | "tokio", 1066 | ] 1067 | 1068 | [[package]] 1069 | name = "tokio-util" 1070 | version = "0.7.4" 1071 | source = "registry+https://github.com/rust-lang/crates.io-index" 1072 | checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" 1073 | dependencies = [ 1074 | "bytes", 1075 | "futures-core", 1076 | "futures-sink", 1077 | "pin-project-lite", 1078 | "tokio", 1079 | "tracing", 1080 | ] 1081 | 1082 | [[package]] 1083 | name = "tower-service" 1084 | version = "0.3.2" 1085 | source = "registry+https://github.com/rust-lang/crates.io-index" 1086 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 1087 | 1088 | [[package]] 1089 | name = "tracing" 1090 | version = "0.1.36" 1091 | source = "registry+https://github.com/rust-lang/crates.io-index" 1092 | checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" 1093 | dependencies = [ 1094 | "cfg-if", 1095 | "pin-project-lite", 1096 | "tracing-attributes", 1097 | "tracing-core", 1098 | ] 1099 | 1100 | [[package]] 1101 | name = "tracing-attributes" 1102 | version = "0.1.22" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" 1105 | dependencies = [ 1106 | "proc-macro2", 1107 | "quote", 1108 | "syn", 1109 | ] 1110 | 1111 | [[package]] 1112 | name = "tracing-core" 1113 | version = "0.1.29" 1114 | source = "registry+https://github.com/rust-lang/crates.io-index" 1115 | checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" 1116 | dependencies = [ 1117 | "once_cell", 1118 | "valuable", 1119 | ] 1120 | 1121 | [[package]] 1122 | name = "tracing-log" 1123 | version = "0.1.3" 1124 | source = "registry+https://github.com/rust-lang/crates.io-index" 1125 | checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" 1126 | dependencies = [ 1127 | "lazy_static", 1128 | "log", 1129 | "tracing-core", 1130 | ] 1131 | 1132 | [[package]] 1133 | name = "tracing-subscriber" 1134 | version = "0.3.15" 1135 | source = "registry+https://github.com/rust-lang/crates.io-index" 1136 | checksum = "60db860322da191b40952ad9affe65ea23e7dd6a5c442c2c42865810c6ab8e6b" 1137 | dependencies = [ 1138 | "ansi_term", 1139 | "sharded-slab", 1140 | "smallvec", 1141 | "thread_local", 1142 | "time", 1143 | "tracing-core", 1144 | "tracing-log", 1145 | ] 1146 | 1147 | [[package]] 1148 | name = "try-lock" 1149 | version = "0.2.3" 1150 | source = "registry+https://github.com/rust-lang/crates.io-index" 1151 | checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" 1152 | 1153 | [[package]] 1154 | name = "ttf-parser" 1155 | version = "0.25.1" 1156 | source = "registry+https://github.com/rust-lang/crates.io-index" 1157 | checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" 1158 | 1159 | [[package]] 1160 | name = "unicode-bidi" 1161 | version = "0.3.8" 1162 | source = "registry+https://github.com/rust-lang/crates.io-index" 1163 | checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" 1164 | 1165 | [[package]] 1166 | name = "unicode-ident" 1167 | version = "1.0.4" 1168 | source = "registry+https://github.com/rust-lang/crates.io-index" 1169 | checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" 1170 | 1171 | [[package]] 1172 | name = "unicode-normalization" 1173 | version = "0.1.22" 1174 | source = "registry+https://github.com/rust-lang/crates.io-index" 1175 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 1176 | dependencies = [ 1177 | "tinyvec", 1178 | ] 1179 | 1180 | [[package]] 1181 | name = "url" 1182 | version = "2.3.1" 1183 | source = "registry+https://github.com/rust-lang/crates.io-index" 1184 | checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" 1185 | dependencies = [ 1186 | "form_urlencoded", 1187 | "idna", 1188 | "percent-encoding", 1189 | ] 1190 | 1191 | [[package]] 1192 | name = "valuable" 1193 | version = "0.1.0" 1194 | source = "registry+https://github.com/rust-lang/crates.io-index" 1195 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 1196 | 1197 | [[package]] 1198 | name = "vcpkg" 1199 | version = "0.2.15" 1200 | source = "registry+https://github.com/rust-lang/crates.io-index" 1201 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1202 | 1203 | [[package]] 1204 | name = "want" 1205 | version = "0.3.0" 1206 | source = "registry+https://github.com/rust-lang/crates.io-index" 1207 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 1208 | dependencies = [ 1209 | "log", 1210 | "try-lock", 1211 | ] 1212 | 1213 | [[package]] 1214 | name = "wasi" 1215 | version = "0.11.0+wasi-snapshot-preview1" 1216 | source = "registry+https://github.com/rust-lang/crates.io-index" 1217 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1218 | 1219 | [[package]] 1220 | name = "wasm-bindgen" 1221 | version = "0.2.83" 1222 | source = "registry+https://github.com/rust-lang/crates.io-index" 1223 | checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" 1224 | dependencies = [ 1225 | "cfg-if", 1226 | "wasm-bindgen-macro", 1227 | ] 1228 | 1229 | [[package]] 1230 | name = "wasm-bindgen-backend" 1231 | version = "0.2.83" 1232 | source = "registry+https://github.com/rust-lang/crates.io-index" 1233 | checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" 1234 | dependencies = [ 1235 | "bumpalo", 1236 | "log", 1237 | "once_cell", 1238 | "proc-macro2", 1239 | "quote", 1240 | "syn", 1241 | "wasm-bindgen-shared", 1242 | ] 1243 | 1244 | [[package]] 1245 | name = "wasm-bindgen-futures" 1246 | version = "0.4.33" 1247 | source = "registry+https://github.com/rust-lang/crates.io-index" 1248 | checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" 1249 | dependencies = [ 1250 | "cfg-if", 1251 | "js-sys", 1252 | "wasm-bindgen", 1253 | "web-sys", 1254 | ] 1255 | 1256 | [[package]] 1257 | name = "wasm-bindgen-macro" 1258 | version = "0.2.83" 1259 | source = "registry+https://github.com/rust-lang/crates.io-index" 1260 | checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" 1261 | dependencies = [ 1262 | "quote", 1263 | "wasm-bindgen-macro-support", 1264 | ] 1265 | 1266 | [[package]] 1267 | name = "wasm-bindgen-macro-support" 1268 | version = "0.2.83" 1269 | source = "registry+https://github.com/rust-lang/crates.io-index" 1270 | checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" 1271 | dependencies = [ 1272 | "proc-macro2", 1273 | "quote", 1274 | "syn", 1275 | "wasm-bindgen-backend", 1276 | "wasm-bindgen-shared", 1277 | ] 1278 | 1279 | [[package]] 1280 | name = "wasm-bindgen-shared" 1281 | version = "0.2.83" 1282 | source = "registry+https://github.com/rust-lang/crates.io-index" 1283 | checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" 1284 | 1285 | [[package]] 1286 | name = "web-sys" 1287 | version = "0.3.60" 1288 | source = "registry+https://github.com/rust-lang/crates.io-index" 1289 | checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" 1290 | dependencies = [ 1291 | "js-sys", 1292 | "wasm-bindgen", 1293 | ] 1294 | 1295 | [[package]] 1296 | name = "weectrl" 1297 | version = "2.0.2" 1298 | dependencies = [ 1299 | "advmac", 1300 | "directories", 1301 | "fltk", 1302 | "fltk-theme", 1303 | "futures", 1304 | "hyper", 1305 | "mime", 1306 | "reqwest", 1307 | "serde", 1308 | "serde-xml-rs", 1309 | "serde_derive", 1310 | "serde_json", 1311 | "tokio", 1312 | "tracing", 1313 | "tracing-subscriber", 1314 | "url", 1315 | ] 1316 | 1317 | [[package]] 1318 | name = "winapi" 1319 | version = "0.3.9" 1320 | source = "registry+https://github.com/rust-lang/crates.io-index" 1321 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1322 | dependencies = [ 1323 | "winapi-i686-pc-windows-gnu", 1324 | "winapi-x86_64-pc-windows-gnu", 1325 | ] 1326 | 1327 | [[package]] 1328 | name = "winapi-i686-pc-windows-gnu" 1329 | version = "0.4.0" 1330 | source = "registry+https://github.com/rust-lang/crates.io-index" 1331 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1332 | 1333 | [[package]] 1334 | name = "winapi-x86_64-pc-windows-gnu" 1335 | version = "0.4.0" 1336 | source = "registry+https://github.com/rust-lang/crates.io-index" 1337 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1338 | 1339 | [[package]] 1340 | name = "windows-sys" 1341 | version = "0.36.1" 1342 | source = "registry+https://github.com/rust-lang/crates.io-index" 1343 | checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" 1344 | dependencies = [ 1345 | "windows_aarch64_msvc 0.36.1", 1346 | "windows_i686_gnu 0.36.1", 1347 | "windows_i686_msvc 0.36.1", 1348 | "windows_x86_64_gnu 0.36.1", 1349 | "windows_x86_64_msvc 0.36.1", 1350 | ] 1351 | 1352 | [[package]] 1353 | name = "windows-sys" 1354 | version = "0.48.0" 1355 | source = "registry+https://github.com/rust-lang/crates.io-index" 1356 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1357 | dependencies = [ 1358 | "windows-targets", 1359 | ] 1360 | 1361 | [[package]] 1362 | name = "windows-targets" 1363 | version = "0.48.5" 1364 | source = "registry+https://github.com/rust-lang/crates.io-index" 1365 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1366 | dependencies = [ 1367 | "windows_aarch64_gnullvm", 1368 | "windows_aarch64_msvc 0.48.5", 1369 | "windows_i686_gnu 0.48.5", 1370 | "windows_i686_msvc 0.48.5", 1371 | "windows_x86_64_gnu 0.48.5", 1372 | "windows_x86_64_gnullvm", 1373 | "windows_x86_64_msvc 0.48.5", 1374 | ] 1375 | 1376 | [[package]] 1377 | name = "windows_aarch64_gnullvm" 1378 | version = "0.48.5" 1379 | source = "registry+https://github.com/rust-lang/crates.io-index" 1380 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1381 | 1382 | [[package]] 1383 | name = "windows_aarch64_msvc" 1384 | version = "0.36.1" 1385 | source = "registry+https://github.com/rust-lang/crates.io-index" 1386 | checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" 1387 | 1388 | [[package]] 1389 | name = "windows_aarch64_msvc" 1390 | version = "0.48.5" 1391 | source = "registry+https://github.com/rust-lang/crates.io-index" 1392 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1393 | 1394 | [[package]] 1395 | name = "windows_i686_gnu" 1396 | version = "0.36.1" 1397 | source = "registry+https://github.com/rust-lang/crates.io-index" 1398 | checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" 1399 | 1400 | [[package]] 1401 | name = "windows_i686_gnu" 1402 | version = "0.48.5" 1403 | source = "registry+https://github.com/rust-lang/crates.io-index" 1404 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1405 | 1406 | [[package]] 1407 | name = "windows_i686_msvc" 1408 | version = "0.36.1" 1409 | source = "registry+https://github.com/rust-lang/crates.io-index" 1410 | checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" 1411 | 1412 | [[package]] 1413 | name = "windows_i686_msvc" 1414 | version = "0.48.5" 1415 | source = "registry+https://github.com/rust-lang/crates.io-index" 1416 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1417 | 1418 | [[package]] 1419 | name = "windows_x86_64_gnu" 1420 | version = "0.36.1" 1421 | source = "registry+https://github.com/rust-lang/crates.io-index" 1422 | checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" 1423 | 1424 | [[package]] 1425 | name = "windows_x86_64_gnu" 1426 | version = "0.48.5" 1427 | source = "registry+https://github.com/rust-lang/crates.io-index" 1428 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1429 | 1430 | [[package]] 1431 | name = "windows_x86_64_gnullvm" 1432 | version = "0.48.5" 1433 | source = "registry+https://github.com/rust-lang/crates.io-index" 1434 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1435 | 1436 | [[package]] 1437 | name = "windows_x86_64_msvc" 1438 | version = "0.36.1" 1439 | source = "registry+https://github.com/rust-lang/crates.io-index" 1440 | checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" 1441 | 1442 | [[package]] 1443 | name = "windows_x86_64_msvc" 1444 | version = "0.48.5" 1445 | source = "registry+https://github.com/rust-lang/crates.io-index" 1446 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1447 | 1448 | [[package]] 1449 | name = "winreg" 1450 | version = "0.10.1" 1451 | source = "registry+https://github.com/rust-lang/crates.io-index" 1452 | checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" 1453 | dependencies = [ 1454 | "winapi", 1455 | ] 1456 | 1457 | [[package]] 1458 | name = "xml-rs" 1459 | version = "0.8.4" 1460 | source = "registry+https://github.com/rust-lang/crates.io-index" 1461 | checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" 1462 | --------------------------------------------------------------------------------