use std::sync::Arc;
use futures::sink::SinkExt;
use futures::stream::StreamExt;
use reqwest::cookie::CookieStore;
use tokio_tungstenite::tungstenite::protocol::Message;
mod error;
pub use error::Error;
#[allow(dead_code)] mod model;
pub use model::TemperatureUnit;
use model::LoginRequest;
pub use model::User;
use model::GetLocationRequest;
pub use model::Location;
use model::LocationResponse;
pub use model::GatewayData;
use model::WsLoginRequest;
use model::DataRequest;
use model::DataResponse;
#[cfg(feature = "clap")]
#[derive(Debug, clap::Parser)]
pub struct SymphonyConfig {
#[clap(long, env = "SYMPHONY_EMAIL")]
email: String,
#[clap(long, env = "SYMPHONY_PASSWORD")]
password: String,
#[clap(long, env = "SYMPHONY_TEMPERATURE_UNIT", default_value = "F")]
temperature_unit: TemperatureUnit
}
#[cfg(feature = "clap")]
impl SymphonyConfig {
pub async fn client(&self) -> Result<Client, Error> {
Client::new(self.email.as_ref(), self.password.as_ref(), self.temperature_unit).await
}
}
pub struct Client {
http: reqwest::Client,
cookies: Arc<reqwest::cookie::Jar>,
user: User
}
impl Client {
pub async fn new(email: &str, password: &str, temperature_unit: TemperatureUnit) -> Result<Self, Error> {
let url = "https://symphony.mygeostar.com/".parse().unwrap();
let cookies = Arc::new(reqwest::cookie::Jar::default());
cookies.add_cookie_str("legal-acknowledge=yes", &url);
cookies.add_cookie_str(temperature_unit.as_cookie_str(), &url);
let http = reqwest::Client::builder().cookie_provider(cookies.clone()).build()?;
http.post("https://symphony.mygeostar.com/account/login").form(&LoginRequest::new(email, password)).send().await?;
let user = http.get("https://symphony.mygeostar.com/api.php/user").send().await?.json().await?;
Ok(Self{
http,
cookies,
user,
})
}
pub fn user(&self) -> &User {
&self.user
}
pub async fn get_locations(&self) -> Result<Vec<Location>, Error> {
let response: LocationResponse = self.http.post("https://symphony.mygeostar.com/api.php/awl/json/location/getlocationdata.php").header("Accept", "application/json").form(&GetLocationRequest{awluserkey: self.user.awl_user_key}).send().await?.json().await?;
Ok(response.locations)
}
pub async fn get_data(&self, awl_id: &str) -> Result<GatewayData, Error> {
let (mut ws, _) = tokio_tungstenite::connect_async("wss://awlclientproxy.mygeostar.com/").await?;
let cookies = self.cookies.cookies(&"https://symphony.mygeostar.com/".parse().unwrap()).ok_or(Error::MissingSessionId)?;
let cookies = std::str::from_utf8(cookies.as_ref()).unwrap();
let sessionid = cookies.split(';').find_map(|s| s.trim_start().strip_prefix("sessionid=")).ok_or(Error::MissingSessionId)?;
let request = WsLoginRequest{
cmd: "login",
tid: 2,
source: "consumer dashboard",
sessionid
};
ws.send(Message::Text(serde_json::to_string(&request).map_err(Error::JsonSerialize)?)).await?;
loop {
match ws.next().await {
Some(Ok(Message::Text(_))) => break, Some(Ok(Message::Ping(_))) => ws.send(Message::Pong(vec![])).await?,
Some(Ok(m)) => return Err(Error::InvalidResponse(m)),
Some(Err(e)) => return Err(e.into()),
None => return Err(Error::WebsocketClosed),
};
}
let request = DataRequest{
cmd: "read",
tid: 1,
awlid: awl_id,
zone: 0,
rlist: &[
"compressorpower",
"fanpower",
"auxpower",
"looppumppower",
"totalunitpower",
"AWLABCType",
"ModeOfOperation",
"ActualCompressorSpeed",
"AirflowCurrentSpeed",
"AuroraOutputEH1",
"AuroraOutputEH2",
"AuroraOutputCC",
"AuroraOutputCC2",
"TStatDehumidSetpoint",
"TStatRelativeHumidity",
"LeavingAirTemp",
"TStatRoomTemp",
"EnteringWaterTemp",
"AOCEnteringWaterTemp",
"auroraoutputrv",
"AWLTStatType",
"humidity_offset_settings",
"iz2_humidity_offset_settings",
"dehumid_humid_sp",
"iz2_dehumid_humid_sp",
"lockoutstatus",
"lastfault",
"lastlockout",
"homeautomationalarm1",
"homeautomationalarm2",
"roomtemp",
"activesettings",
"TStatActiveSetpoint",
"TStatMode",
"TStatHeatingSetpoint",
"TStatCoolingSetpoint"
],
source: "consumer dashboard"
};
ws.send(Message::Text(serde_json::to_string(&request).map_err(Error::JsonSerialize)?)).await?;
let response: DataResponse = loop {
match ws.next().await {
Some(Ok(Message::Text(s))) => break serde_json::from_str(&s).map_err(Error::JsonDeserialize)?,
Some(Ok(Message::Ping(_))) => ws.send(Message::Pong(vec![])).await?,
Some(Ok(m)) => return Err(Error::InvalidResponse(m)),
Some(Err(e)) => return Err(e.into()),
None => return Err(Error::WebsocketClosed),
};
};
Ok(response.into())
}
}