tonik/
lib.rs

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    /// Send a POST request to the router.
35    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    /// Send a GET request to the router.
62    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    // rrm
140    pub bitrate: i64,
141    pub name: String,
142    // airtime
143    // ...
144    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}