1use crate::auth::AuthState;
4use crate::error::{PolestarError, Result};
5use crate::graphql;
6use crate::models::{telemetry::Telemetry, vehicle::Vehicle};
7use std::sync::Arc;
8
9#[derive(Clone)]
24pub struct PolestarClient {
25 http_client: reqwest::Client,
26 auth_state: Arc<AuthState>,
27 pc_api_base: String,
28 cms_api_base: String,
29}
30
31impl PolestarClient {
32 pub fn new(username: impl Into<String>, password: impl Into<String>) -> Result<Self> {
49 let http_client = reqwest::Client::builder()
50 .cookie_store(true)
51 .timeout(std::time::Duration::from_secs(30))
52 .build()?;
53
54 let auth_state = Arc::new(AuthState::new(username.into(), password.into()));
55
56 Ok(Self {
57 http_client,
58 auth_state,
59 pc_api_base: "https://pc-api.polestar.com/eu-north-1/mystar-v2".to_string(),
60 cms_api_base: "https://cms-api.polestar.com/".to_string(),
61 })
62 }
63
64 pub async fn get_telemetry(&self, vin: &str) -> Result<Telemetry> {
83 let variables = serde_json::json!({
84 "vin": vin
85 });
86
87 self.post_graphql(&self.pc_api_base, graphql::queries::CAR_TELEMETRICS_V2, variables)
88 .await
89 }
90
91 pub async fn get_vehicle(&self, vin: &str) -> Result<Vehicle> {
97 let variables = serde_json::json!({});
98
99 let token = self.authenticate().await?;
101
102 let body = serde_json::json!({
103 "query": graphql::queries::GET_CONSUMER_CARS_V2,
104 "variables": variables
105 });
106
107 let response = self
108 .http_client
109 .post(&self.pc_api_base)
110 .header("authorization", format!("Bearer {}", token))
111 .header("content-type", "application/json")
112 .header("origin", "https://www.polestar.com")
113 .json(&body)
114 .send()
115 .await?;
116
117 let json: serde_json::Value = response.json().await?;
118
119 if let Some(errors) = json.get("errors") {
121 if let Some(message) = errors.get(0).and_then(|e| e.get("message")) {
122 return Err(PolestarError::GraphQLError(message.to_string()));
123 }
124 }
125
126 let vehicles_data = json
128 .get("data")
129 .and_then(|d| d.get("getConsumerCarsV2"))
130 .ok_or_else(|| PolestarError::ApiError("No getConsumerCarsV2 field in response".to_string()))?;
131
132 let vehicles: Vec<Vehicle> = serde_json::from_value(vehicles_data.clone())?;
133
134 vehicles
136 .into_iter()
137 .find(|v| v.vin == vin)
138 .ok_or_else(|| PolestarError::InvalidVin(format!("VIN {} not found", vin)))
139 }
140
141 async fn authenticate(&self) -> Result<String> {
146 self.auth_state.get_valid_token(&self.http_client).await
147 }
148
149 async fn post_graphql<T>(
151 &self,
152 endpoint: &str,
153 query: &str,
154 variables: serde_json::Value,
155 ) -> Result<T>
156 where
157 T: serde::de::DeserializeOwned,
158 {
159 let token = self.authenticate().await?;
161
162 let body = serde_json::json!({
163 "query": query,
164 "variables": variables
165 });
166
167 let response = self
168 .http_client
169 .post(endpoint)
170 .header("authorization", format!("Bearer {}", token))
171 .header("content-type", "application/json")
172 .header("origin", "https://www.polestar.com")
173 .json(&body)
174 .send()
175 .await?;
176
177 let status = response.status();
179 if status == reqwest::StatusCode::UNAUTHORIZED {
180 return Err(PolestarError::AuthError(
181 "Invalid credentials or expired session".to_string(),
182 ));
183 }
184 if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
185 return Err(PolestarError::RateLimitExceeded);
186 }
187
188 let json: serde_json::Value = response.json().await?;
190
191 if let Some(errors) = json.get("errors") {
193 if let Some(message) = errors.get(0).and_then(|e| e.get("message")) {
194 return Err(PolestarError::GraphQLError(message.to_string()));
195 }
196 }
197
198 let data = json
200 .get("data")
201 .ok_or_else(|| PolestarError::ApiError("No data field in response".to_string()))?;
202
203 serde_json::from_value(data.clone()).map_err(|e| e.into())
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn test_client_creation() {
213 let client = PolestarClient::new("user@example.com", "password");
214 assert!(client.is_ok());
215 }
216}