diff --git a/Cargo.lock b/Cargo.lock index fcc6a40..d796a83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -460,6 +460,7 @@ dependencies = [ "serde", "serde_json", "ureq", + "xflags", ] [[package]] @@ -681,3 +682,18 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "xflags" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf19f5031a1a812e96fede16f8161218883079946cea87619d3613db1efd268" +dependencies = [ + "xflags-macros", +] + +[[package]] +name = "xflags-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2afbd7f2039bb6cad2dd45f0c5dff49c0d4e26118398768b7a605524d4251809" diff --git a/Cargo.toml b/Cargo.toml index 7d5b989..99f3a10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ paho-mqtt = "*" serde = { version = "*", features = [ "derive" ] } serde_json = "*" ureq = { version = "*", features = [ "json" ] } +xflags = "*" [profile.release] lto = true diff --git a/README.md b/README.md index 835c481..d4634a7 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ solax2mqtt//SENSOR/inverter { Most of the data for this project came from [`squishykid/solax`](https://github.com/squishykid/solax). -Configuration is currently done by editing the source code and recompiling. +## Alternatives PocketWIFI is dreadfully insecure. Use Modbus-RTU or, at the very least, a PocketLAN, if you can. I'll be switching as soon as I'm able to. @@ -48,9 +48,29 @@ This is effectively a custom PocketWIFI module built out of an ESP8266. It's possible I could put one of these together and make it talk MQTT instead of having this separate daemon running. One for later. -The PocketWIFI squirts MQTTS to their cloud servers, but they don't verify TLS -so it can be trivially intercepted. However, the messages are in a useless (to -me) binary format. +The PocketWIFI squirts MQTTS to their cloud servers and don't verify TLS, so it +can be trivially intercepted. However, the messages are in a useless (to me) +binary format. + +## Usage + +First, get TCP access to the PocketWIFI module. Over the AP, it's at `5.8.8.8`. +If you tell it to connect to your WIFI network, your DHCP server will assign an +IP. Next, run `solax2mqtt`: + +``` +solax2mqtt --mqtt-url tcp://: \ + --solax-url http://[:port] \ + --solax-password +``` + +That's it. Errors will be outputted to the console; failure to connect to the +Solax is not fatal (it will keep retrying). Failure to connect to the MQTT +server is (the process will exit). + +You can also solax2mqtt --help` to see a few other options. + +## `mqtt2prometheus` Once the metrics are in MQTT, they can be sent to prometheus using [`mqtt2prometheus`](https://github.com/hikhvar/mqtt2prometheus): diff --git a/src/main.rs b/src/main.rs index 0f5ecb5..2256b2f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,11 +2,9 @@ extern crate paho_mqtt as mqtt; extern crate serde; extern crate serde_json; extern crate ureq; +extern crate xflags; -use std::{ - result::Result, - time::Duration, -}; +use std::{result::Result, time::Duration}; use serde::{Deserialize, Serialize}; @@ -42,13 +40,6 @@ impl From for Error { } } -// Change these for your situation -const DFLT_BROKER: &str = "tcp://10.0.0.1:1883"; -const DFLT_CLIENT: &str = "solax2mqtt"; -const QOS: i32 = 1; -const HTTP_ENDPOINT: &str = "http://5.8.8.8"; -const HTTP_BODY: &str = "optType=ReadRealTimeData&pwd=SXxxxxxxxx"; - #[derive(Deserialize)] struct SolaxResponse { sn: String, @@ -187,10 +178,10 @@ impl TryFrom for MQTTData { } } -fn mqtt_connect() -> Result { +fn mqtt_connect(uri: String, client_id: String) -> Result { let mqtt_opts = mqtt::CreateOptionsBuilder::new() - .server_uri(DFLT_BROKER.to_string()) - .client_id(DFLT_CLIENT.to_string()) + .server_uri(uri) + .client_id(client_id) .finalize(); // Create a client. @@ -212,19 +203,20 @@ fn mqtt_connect() -> Result { return Ok(mqtt_client); } -fn gather() -> Result { - let response = ureq::post(HTTP_ENDPOINT).send_string(HTTP_BODY)?; +fn gather(uri: &str, password: &str) -> Result { + let body = format!("optType=ReadRealTimeData&pwd={}", password); + let response = ureq::post(uri).send_string(&body)?; let solax_response: SolaxResponse = response.into_json()?; solax_response.try_into() } -fn publish(mqtt_client: &mqtt::Client, data: MQTTData) -> Option { +fn publish(mqtt_client: &mqtt::Client, data: MQTTData, qos: i32) -> Option { let content = serde_json::to_string(&data).ok()?; let msg = mqtt::Message::new( format!("solax2mqtt/{}/SENSOR/inverter", data.inverter.adapter_sn), content, - QOS, + qos, ); if let Err(e) = mqtt_client.publish(msg) { @@ -235,12 +227,28 @@ fn publish(mqtt_client: &mqtt::Client, data: MQTTData) -> Option { } fn main() { - let mqtt_client = mqtt_connect().unwrap(); + let flags = xflags::parse_or_exit! { + /// URL of the MQTT server to publish to, e.g. tcp://10.0.0.1:1883 + required --mqtt-url url: String + /// Client ID to use when connecting to the MQTT server. Defaults to solax2mqtt + optional --mqtt-client-id id: String + /// QOS class to publish the messages as. Defaults to 1 + optional --mqtt-qos qos: i32 + /// URL of the Solax PocketWIFI adapter. Defaults to http://5.8.8.8 + optional --solax-url url: String + /// Admin password of the PocketWIFI adapter. Often its serial number + required --solax-password password: String + }; + let solax_url = flags.solax_url.unwrap_or("http://5.8.8.8".to_string()); + let mqtt_client_id = flags.mqtt_client_id.unwrap_or("solax2mqtt".to_string()); + let mqtt_qos = flags.mqtt_qos.unwrap_or(1); + + let mqtt_client = mqtt_connect(flags.mqtt_url, mqtt_client_id).unwrap(); loop { - match gather() { + match gather(&solax_url, &flags.solax_password) { Ok(msg) => { - if let Some(e) = publish(&mqtt_client, msg) { + if let Some(e) = publish(&mqtt_client, msg, mqtt_qos) { println!("Failed to publish data to MQTT, exiting: {:?}", e); break; }