summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorToby Vincent <tobyv@tobyvin.dev>2024-08-20 18:49:11 -0500
committerToby Vincent <tobyv@tobyvin.dev>2024-08-20 18:49:11 -0500
commitaa69b53ccb56f55c0f611953349a5b9f08be7b08 (patch)
tree93cd5cc7e6cf4272e154ad60d0f5e05d2a65d294
parent2710dae2d886fd2a9e949e2a343de5f343bb340d (diff)
wip: more work on protocol
-rw-r--r--Cargo.lock174
-rw-r--r--Cargo.toml2
-rw-r--r--src/discover.rs69
-rw-r--r--src/error.rs6
-rw-r--r--src/lib.rs79
-rw-r--r--src/main.rs28
-rw-r--r--src/protocol.rs97
-rw-r--r--src/types.rs65
8 files changed, 478 insertions, 42 deletions
diff --git a/Cargo.lock b/Cargo.lock
index daeaedf..e80b4b1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 9a2d0b0..88b2cdc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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),
}
diff --git a/src/lib.rs b/src/lib.rs
index faceb2c..174913a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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,
+}