diff options
-rw-r--r-- | Cargo.lock | 174 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | src/discover.rs | 69 | ||||
-rw-r--r-- | src/error.rs | 6 | ||||
-rw-r--r-- | src/lib.rs | 79 | ||||
-rw-r--r-- | src/main.rs | 28 | ||||
-rw-r--r-- | src/protocol.rs | 97 | ||||
-rw-r--r-- | src/types.rs | 65 |
8 files changed, 478 insertions, 42 deletions
@@ -3,17 +3,92 @@ version = 3 [[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "local-ip-address" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136ef34e18462b17bf39a7826f8f3bbc223341f8e83822beb8b77db9a3d49696" +dependencies = [ + "libc", + "neli", + "thiserror", + "windows-sys", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] name = "main_error" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "155db5e86c6e45ee456bf32fad5a290ee1f7151c2faca27ea27097568da67d1a" [[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "neli" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1100229e06604150b3becd61a4965d5c70f3be1759544ea7274166f4be41ef43" +dependencies = [ + "byteorder", + "libc", + "log", + "neli-proc-macros", +] + +[[package]] +name = "neli-proc-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c168194d373b1e134786274020dae7fc5513d565ea2ebb9bc9ff17ffb69106d4" +dependencies = [ + "either", + "proc-macro2", + "quote", + "serde", + "syn 1.0.109", +] + +[[package]] name = "nrgmon" version = "0.1.0" dependencies = [ + "local-ip-address", "main_error", "serde", + "serde_json", "thiserror", ] @@ -36,6 +111,12 @@ dependencies = [ ] [[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] name = "serde" version = "1.0.208" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -52,7 +133,30 @@ checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.75", +] + +[[package]] +name = "serde_json" +version = "1.0.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] @@ -83,7 +187,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.75", ] [[package]] @@ -91,3 +195,69 @@ name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" @@ -4,6 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] +local-ip-address = "0.6.1" main_error = "0.1.2" serde = { version = "1.0.208", features = ["derive"] } +serde_json = "1.0.125" thiserror = "1.0.63" diff --git a/src/discover.rs b/src/discover.rs new file mode 100644 index 0000000..498d42a --- /dev/null +++ b/src/discover.rs @@ -0,0 +1,69 @@ +use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream, UdpSocket}; + +use local_ip_address::list_afinet_netifas; + +use crate::{protocol::SHomeProtocol, types::Data, Device, Error}; + +fn discover_on_interface( + ip: Ipv4Addr, + timeout: Option<std::time::Duration>, +) -> Result<Vec<(SocketAddr, Data)>, Error> { + let bytes = <TcpStream as SHomeProtocol>::encrypt(r#"{"system":{"get_sysinfo":{}}}"#); + let sock = UdpSocket::bind((IpAddr::V4(ip), 0))?; + sock.set_broadcast(true)?; + sock.set_read_timeout(timeout)?; + + for _ in 0..3 { + let _send_res = sock.send_to(&bytes[4..bytes.len()], (Ipv4Addr::BROADCAST, 9999)); + } + + let mut buf = [0_u8; 4096]; + let mut devices = Vec::new(); + while let Ok((_, addr)) = sock.recv_from(&mut buf) { + let msg = <TcpStream as SHomeProtocol>::decrypt(&buf)?; + if let Ok(data) = serde_json::from_str::<Data>(&msg) { + devices.push((addr, data)); + } + } + + Ok(devices) +} + +pub fn discover() -> Result<Vec<Device<TcpStream>>, Error> { + let timeout = Some(std::time::Duration::from_secs(5)); + + let ifaces = list_afinet_netifas()?; + Ok(std::thread::scope(|s| { + let handles = ifaces + .into_iter() + .filter_map(|(_, ip_addr)| match ip_addr { + IpAddr::V4(ipv4_addr) if !ipv4_addr.is_loopback() => { + Some(s.spawn(move || discover_on_interface(ipv4_addr, timeout))) + } + _ => None, + }) + .collect::<Vec<_>>(); + + handles + .into_iter() + .flat_map(|join_handle| join_handle.join()) + .flatten() + .flatten() + .inspect(|(_, data)| { + dbg!(data); + }) + .flat_map(TryFrom::try_from) + .collect::<Vec<_>>() + })) +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + + #[test] + fn test_discovery() -> Result<(), Error> { + let _ = discover()?; + Ok(()) + } +} diff --git a/src/error.rs b/src/error.rs index ec03758..a4ab70b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -7,4 +7,10 @@ pub enum Error { #[error("Invalid UTF-8: {0}")] Utf8(#[from] std::str::Utf8Error), + + #[error("Invalid Json: {0}")] + Json(#[from] serde_json::Error), + + #[error("Error getting local network interfaces: {0}")] + Iface(#[from] local_ip_address::Error), } @@ -1,4 +1,81 @@ -pub use error::{Error, Result}; +use std::net::{TcpStream, ToSocketAddrs}; +use protocol::SHomeProtocol; +use types::{Data, SysInfo}; + +pub use crate::{ + discover::discover, + error::{Error, Result}, +}; + +pub mod discover; pub mod error; pub mod protocol; +pub mod types; + +#[derive(Debug, Clone)] +pub struct Device<P> +where + P: SHomeProtocol, +{ + protocol: P, + pub sysinfo: SysInfo, +} + +impl<P> Device<P> +where + P: SHomeProtocol, +{ + pub fn new(mut protocol: P) -> Result<Self, Error> { + protocol.send(r#"{"system":{"get_sysinfo":{}}}"#)?; + let data: Data = serde_json::from_str(&protocol.recv()?)?; + + Ok(Self { + protocol, + sysinfo: data.system.sysinfo, + }) + } + + pub fn send(&mut self, msg: &str) -> Result<String, Error> { + self.protocol.send(msg)?; + self.protocol.recv() + } +} + +impl Device<TcpStream> { + pub fn connect<A>(addr: A) -> Result<Self, Error> + where + A: ToSocketAddrs, + { + Self::new(TcpStream::connect(addr)?) + } +} + +impl<A> TryFrom<(A, Data)> for Device<TcpStream> +where + A: ToSocketAddrs, +{ + type Error = std::io::Error; + + fn try_from((addr, data): (A, Data)) -> Result<Self, Self::Error> { + TcpStream::connect(addr).map(|protocol| Self { + protocol, + sysinfo: data.system.sysinfo, + }) + } +} + +//macro_rules! command { +// ($name:ident, $fmt:expr) => { ... }; +// ($name:ident, $fmt:expr, $($args:tt)*) => { +// pub fn $name($($a: $t),*) -> Result<(), Error> { +// +// } +// }; +// ($name:ident, $( $a:ident: $t:ty ),*, $msg:literal) => { +// pub fn $name($($a: $t),*) -> Result<(), Error> { +// +// } +// +// }; +//} diff --git a/src/main.rs b/src/main.rs index 05ea213..347695c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,31 @@ +use nrgmon::Device; + fn main() -> Result<(), main_error::MainError> { + let devices = nrgmon::discover()?; + for device in devices { + let json = serde_json::to_string_pretty(&device.sysinfo)?; + println!("{json}"); + } + + let mut device = Device::connect("10.42.0.119:9999")?; + let json = serde_json::to_string_pretty(&device.sysinfo)?; + println!("{json}"); + + let msg = r#"{"emeter":{"get_daystat":{"month":7,"year":2024}}"#; + let resp = device.send(msg)?; + println!("{resp}"); + + //let msg = + // r#"{"emeter":{"get_daystat":{"month":7,"year":2024},"context":{"child_ids":["XXX"]}}"#; + //let children = device.sysinfo.children.clone(); + //for child in children.into_iter().flatten() { + // let resp = device.send(&msg.replace("XXX", &child.id))?; + // println!("{resp}"); + //} + + //let resp = device.send( + // r#"{"context":{"child_ids":["80060951C814E6584DE333F082C11AB71D24786C05"]},"system":{"set_relay_state":{"state":1}}}"#, + //)?; + //println!("{resp}"); Ok(()) } diff --git a/src/protocol.rs b/src/protocol.rs index 9e5edbe..54a343c 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -1,58 +1,77 @@ -use crate::Error; - -const KEY: u8 = 0xAB; - -pub fn encrypt(msg: &mut [u8]) { - let mut key = KEY; - msg.iter_mut().for_each(|b| { - key ^= *b; - *b = key; - }); -} +use std::io::{Read, Write}; -pub fn decrypt(bytes: &mut [u8]) { - let mut key = KEY; - bytes.iter_mut().for_each(|b| { - let xor = *b ^ key; - key = *b; - *b = xor; - }); -} - -pub fn recv<R: std::io::Read>(r: &mut R) -> Result<String, Error> { - let mut buf = [0; 4]; - r.read_exact(&mut buf)?; +use crate::Error; - let mut buf = vec![0; u32::from_be_bytes(buf) as usize]; - r.read_exact(&mut buf)?; +pub const INFO_MSG: &[u8; 29] = br#"{"system":{"get_sysinfo":{}}}"#; - decrypt(&mut buf); +pub trait SHomeProtocol { + fn recv(&mut self) -> Result<String, Error>; + fn send(&mut self, msg: &str) -> Result<(), Error>; - let msg = std::str::from_utf8(&buf)?.to_string(); + fn encrypt(msg: &str) -> Vec<u8> { + msg.as_bytes() + .iter() + .scan(0xAB, |key, b| { + *key ^= *b; + Some(*key) + }) + .collect() + } - Ok(msg) + fn decrypt(bytes: &[u8]) -> Result<String, Error> { + let buf: Vec<u8> = bytes + .iter() + .scan(0xAB, |key, b| { + let xor = *b ^ *key; + *key = *b; + Some(xor) + }) + .collect(); + + Ok(std::str::from_utf8(&buf)?.to_owned()) + } } -pub fn send<W: std::io::Write>(w: &mut W, msg: &str) -> Result<(), Error> { - let mut buf = msg.as_bytes().to_owned(); - encrypt(&mut buf); +//impl SHomeProtocol for UdpSocket { +// fn recv(&mut self) -> Result<String, Error> { +// todo!() +// } +// +// fn send(&mut self, msg: &str) -> Result<(), Error> { +// todo!() +// } +//} + +impl<T: Read + Write> SHomeProtocol for T { + fn recv(&mut self) -> Result<String, Error> { + let mut buf = [0; 4]; + self.read_exact(&mut buf)?; + + let mut buf = vec![0; u32::from_be_bytes(buf) as usize]; + self.read_exact(&mut buf)?; + + Self::decrypt(&buf) + } + + fn send(&mut self, msg: &str) -> Result<(), Error> { + let buf = Self::encrypt(msg); - w.write_all(&(buf.len() as u32).to_be_bytes())?; - w.write_all(&buf)?; - Ok(()) + self.write_all(&(buf.len() as u32).to_be_bytes())?; + self.write_all(&buf)?; + Ok(()) + } } #[cfg(test)] pub(crate) mod tests { - use super::*; + use crate::Device; - use std::net::TcpStream; + use super::*; #[test] fn test_get_sysinfo() -> Result<(), Error> { - let mut stream = TcpStream::connect("10.42.0.119:9999")?; - send(&mut stream, r#"{"system":{"get_sysinfo":{}}}"#)?; - let msg = recv(&mut stream)?; + let mut device = Device::connect("10.42.0.119:9999")?; + let msg = device.send(r#"{"system":{"get_sysinfo":{}}}"#)?; assert!(!msg.is_empty()); diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..1409f5f --- /dev/null +++ b/src/types.rs @@ -0,0 +1,65 @@ +use serde::{Deserialize, Serialize}; +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Data { + pub system: System, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct System { + #[serde(rename = "get_sysinfo")] + pub sysinfo: SysInfo, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct SysInfo { + pub sw_ver: String, + pub hw_ver: String, + #[serde(alias = "type")] + #[serde(alias = "mic_type")] + pub hw_type: String, + pub model: String, + #[serde(alias = "mic_mac")] + pub mac: String, + #[serde(rename = "deviceId")] + pub device_id: String, + #[serde(rename = "hwId")] + pub hw_id: String, + #[serde(rename = "oemId")] + pub oem_id: String, + pub alias: String, + #[serde(alias = "description")] + pub dev_name: Option<String>, + pub err_code: i16, + pub rssi: i32, + pub active_mode: Option<String>, // TODO: Could be enum + + #[serde(rename = "fwId")] + pub fw_id: Option<String>, + pub relay_state: Option<u8>, + pub on_time: Option<i64>, + pub feature: Option<String>, // TODO: Could be enum + pub updating: Option<u8>, + pub icon_hash: Option<String>, + pub led_off: Option<u8>, + + // HS100 + pub longitude_i: Option<i32>, + pub latitude_i: Option<i32>, + pub ntc_state: Option<u8>, // TODO: what is this? + + // HS110 + pub longitude: Option<f64>, + pub latitude: Option<f64>, + + // HS300 + pub children: Option<Vec<SysInfoChild>>, + pub child_num: Option<u8>, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct SysInfoChild { + pub id: String, + pub state: u8, + pub alias: String, + pub on_time: u64, +} |