1use std::{
2 collections::HashMap,
3 fmt::{self, Display, Formatter},
4};
5
6use serde::{de::DeserializeOwned, Deserialize, Serialize};
7use serde_json::json;
8
9pub struct TeltonikaClient {
10 host: String,
11 reqwest: reqwest::Client,
12 auth: Option<LoginData>,
13}
14
15impl TeltonikaClient {
16 pub fn new(host: String) -> Self {
17 TeltonikaClient {
18 host,
19 reqwest: reqwest::Client::builder().gzip(true).build().unwrap(),
20 auth: None,
21 }
22 }
23
24 pub async fn authenticate(
25 &mut self,
26 username: &str,
27 password: &str,
28 ) -> Result<Response<LoginData>, reqwest::Error> {
29 let response = self.login(username, password).await?;
30 self.auth = response.data.clone();
31 Ok(response)
32 }
33
34 pub async fn post<R, T>(
36 &self,
37 path: &str,
38 body: Option<R>,
39 ) -> Result<Response<T>, reqwest::Error>
40 where
41 R: Serialize,
42 T: DeserializeOwned,
43 {
44 let mut request = self
45 .reqwest
46 .post(format!("http://{}/api{}", self.host, path).as_str());
47
48 if let Some(auth) = self.auth.as_ref() {
49 request = request.bearer_auth(auth.token.as_str());
50 }
51
52 if let Some(body) = body {
53 request = request.json(&body);
54 }
55
56 let response = request.send().await?.json::<Response<T>>().await?;
57
58 Ok(response)
59 }
60
61 pub async fn get<T>(&self, path: &str) -> Result<Response<T>, reqwest::Error>
63 where
64 T: DeserializeOwned,
65 {
66 let mut request = self
67 .reqwest
68 .get(format!("http://{}/api{}", self.host, path).as_str());
69
70 if let Some(auth) = self.auth.as_ref() {
71 request = request.bearer_auth(auth.token.as_str());
72 }
73
74 let response = request.send().await?.json::<Response<T>>().await?;
75
76 Ok(response)
77 }
78
79 pub async fn login(
80 &self,
81 username: &str,
82 password: &str,
83 ) -> Result<Response<LoginData>, reqwest::Error> {
84 self.post(
85 "/login",
86 Some(&json!({
87 "username": username,
88 "password": password,
89 })),
90 )
91 .await
92 }
93
94 pub async fn dhcp_leases_ipv4_status(
95 &self,
96 ) -> Result<Response<Vec<DhcpLease>>, reqwest::Error> {
97 self.get("/dhcp/leases/ipv4/status").await
98 }
99
100 pub async fn firmware_device_status(
101 &self,
102 ) -> Result<Response<FirmwareDeviceStatus>, reqwest::Error> {
103 self.get("/firmware/device/status").await
104 }
105
106 pub async fn firmware_actions_fota_download(&self) -> Result<Response<()>, reqwest::Error> {
107 self.post("/firmware/actions/fota_download", None::<()>)
108 .await
109 }
110
111 pub async fn gps_position_status(&self) -> Result<Response<GpsPositionStatus>, reqwest::Error> {
112 self.get("/gps/position/status").await
113 }
114
115 pub async fn wireless_devices_status(
116 &self,
117 ) -> Result<Response<Vec<WirelessDeviceStatus>>, reqwest::Error> {
118 self.get("/wireless/devices/status").await
119 }
120
121 pub async fn wireless_interfaces_status(
122 &self,
123 ) -> Result<Response<Vec<InterfaceStatus>>, reqwest::Error> {
124 self.get("/wireless/interfaces/status").await
125 }
126}
127
128#[derive(Debug, Deserialize, Serialize)]
129pub struct InterfaceStatus {
130 pub ifname: String,
131 pub disabled: bool,
132 pub op_class: i64,
133 pub status: String,
134 pub quality: i64,
135 pub noise: i64,
136 pub up: bool,
137 pub device: InterfaceStatusDevice,
138 pub txpoweroff: i64,
139 pub bitrate: i64,
141 pub name: String,
142 pub ssid: String,
145 pub assoclist: HashMap<String, InterfaceStatusAssoc>,
146}
147
148#[derive(Debug, Deserialize, Serialize)]
149pub struct InterfaceStatusAssoc {
150 pub signal: i64,
151}
152
153#[derive(Debug, Deserialize, Serialize)]
154pub struct InterfaceStatusDevice {
155 device: String,
156 pending: bool,
157 name: String,
158 up: bool,
159}
160
161#[derive(Debug, Deserialize, Serialize)]
162pub struct WirelessDeviceStatus {
163 pub id: String,
164 pub quality_max: i64,
165}
166
167#[derive(Debug, Deserialize, Serialize)]
168pub struct GpsPositionStatus {
169 accuracy: String,
170 fix_status: String,
171 altitude: String,
172 timestamp: String,
173 satellites: String,
174 longitude: String,
175 latitude: String,
176 angle: String,
177 utc_timestamp: String,
178}
179
180impl Display for GpsPositionStatus {
181 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
182 write!(
183 f,
184 "Accuracy: {}\nFix status: {}\nAltitude: {}\nTimestamp: {}\nSatellites: {}\nLongitude: {}\nLatitude: {}\nAngle: {}\nUTC timestamp: {}",
185 self.accuracy, self.fix_status, self.altitude, self.timestamp, self.satellites, self.longitude, self.latitude, self.angle, self.utc_timestamp
186 )
187 }
188}
189
190#[derive(Debug, Deserialize, Serialize)]
191pub struct FirmwareDeviceStatus {
192 pub kernel_version: String,
193 pub version: String,
194 pub build_date: String,
195}
196
197impl Display for FirmwareDeviceStatus {
198 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
199 write!(
200 f,
201 "Kernel version: {}\nVersion: {}\nBuild date: {}",
202 self.kernel_version, self.version, self.build_date
203 )
204 }
205}
206
207#[derive(Debug, Deserialize, Serialize)]
208pub struct DhcpLease {
209 pub expires: i64,
210 pub macaddr: String,
211 pub ipaddr: String,
212 pub hostname: Option<String>,
213}
214
215impl Display for DhcpLease {
216 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
217 writeln!(
218 f,
219 "MAC address: {}\nIP address: {}\nHostname: {}\nExpires: {}",
220 self.macaddr,
221 self.ipaddr,
222 self.hostname.as_deref().unwrap_or(""),
223 self.expires
224 )?;
225
226 Ok(())
227 }
228}
229
230#[derive(Debug, Deserialize, Serialize, Clone)]
231pub struct LoginData {
232 pub username: String,
233 pub token: String,
234 pub expires: i64,
235}
236
237#[derive(Debug, Deserialize, Serialize)]
238pub struct Response<T> {
239 pub success: bool,
240 pub data: Option<T>,
241 pub errors: Option<Vec<ApiError>>,
242}
243
244#[derive(Debug, Deserialize, Serialize)]
245pub struct ApiError {
246 pub code: i32,
247 pub error: String,
248 pub source: String,
249 pub section: Option<String>,
250}
251
252#[cfg(test)]
253mod tests {
254
255 use std::env;
256
257 use super::*;
258
259 fn create_client() -> TeltonikaClient {
260 TeltonikaClient::new(env::var("TELTONIKA_HOST").expect("TELTONIKA_HOST is not set"))
261 }
262
263 async fn create_authenticated_client() -> TeltonikaClient {
264 let mut client = create_client();
265 let response = client
266 .authenticate(
267 env::var("TELTONIKA_USERNAME")
268 .expect("TELTONIKA_USERNAME is not set")
269 .as_str(),
270 env::var("TELTONIKA_PASSWORD")
271 .expect("TELTONIKA_PASSWORD is not set")
272 .as_str(),
273 )
274 .await
275 .unwrap();
276
277 assert!(response.success);
278 assert!(response.data.is_some());
279
280 client
281 }
282
283 #[tokio::test]
284 async fn test_login() {
285 create_authenticated_client().await;
286 }
287
288 #[tokio::test]
289 async fn test_dhcp_leases_ipv4_status() {
290 let client = create_authenticated_client().await;
291 let response = client.dhcp_leases_ipv4_status().await.unwrap();
292
293 assert!(response.success);
294 assert!(response.data.is_some());
295 }
296
297 #[tokio::test]
298 async fn test_firmware_device_status() {
299 let client = create_authenticated_client().await;
300 let response = client.firmware_device_status().await.unwrap();
301
302 assert!(response.success);
303 assert!(response.data.is_some());
304 }
305}