islabtech_upw_sensor_v1/device/
network.rs

1//! implements connections to a [`Device`](crate::Device) via a network connection
2use serde::Deserialize;
3
4use crate::{
5    measurements::{Measurement, SuccessfulMeasurement},
6    SystemStatus,
7};
8
9/// UPW Sensor device connected via a network connection
10pub struct NetworkConnectedDevice {
11    ip_address: std::net::IpAddr,
12    port: u16,
13    use_tls: bool,
14}
15
16/// wrapper type for parameter TCP port. The default is port 80 (HTTP).
17pub struct TcpPort(u16);
18impl From<u16> for TcpPort {
19    fn from(value: u16) -> Self {
20        TcpPort(value)
21    }
22}
23impl Default for TcpPort {
24    fn default() -> Self {
25        80.into()
26    }
27}
28
29/// (mostly wrapper) type for parameter TLS usage. The default is [`UseTls::Auto`].
30pub enum UseTls {
31    /// do not use TLS
32    Off,
33    /// enforce TLS
34    On,
35    /// decide based on port number whether to use TLS
36    Auto,
37}
38impl From<bool> for UseTls {
39    fn from(value: bool) -> Self {
40        if value {
41            return UseTls::On;
42        } else {
43            return UseTls::Off;
44        }
45    }
46}
47impl Default for UseTls {
48    fn default() -> Self {
49        UseTls::Auto
50    }
51}
52
53// constructors
54pub fn connect_via_network(
55    ip_address: std::net::IpAddr,
56    use_tls: UseTls,
57) -> NetworkConnectedDevice {
58    connect_via_network_on_port(ip_address, 80.into(), use_tls)
59}
60pub fn connect_via_network_on_port(
61    ip_address: std::net::IpAddr,
62    port: TcpPort,
63    use_tls: UseTls,
64) -> NetworkConnectedDevice {
65    NetworkConnectedDevice {
66        ip_address,
67        port: port.0,
68        use_tls: match use_tls {
69            UseTls::Off => false,
70            UseTls::On => true,
71            UseTls::Auto => port.0 == 443,
72        },
73    }
74}
75
76// implementation
77impl NetworkConnectedDevice {
78    /// returns the API base url without a path, e.g. `http://192.168.1.123:80`
79    fn base_url(&self) -> String {
80        format!(
81            "{proto}://{addr}:{port}",
82            proto = if self.use_tls { "https" } else { "http" },
83            addr = self.ip_address.to_string(),
84            port = self.port,
85        )
86    }
87}
88/// queries an API endpoint and deserializes the response
89///
90/// Examples:
91///  ```rust,ignore
92/// # use islabtech_upw_sensor_v1::{connect_via_network_on_port,Measurement};
93/// # use islabtech_upw_sensor_v1::device::network::fetch;
94/// let sensor = connect_via_network_on_port(
95///     "192.168.1.123".parse().unwrap(),
96///     80.into(),
97///     Default::default(),
98/// );
99/// let result: Measurement = fetch::<Measurement>(&sensor, "/api/v1/measurements/latest").await?;
100/// ```
101async fn fetch<T: serde::de::DeserializeOwned>(
102    device: &NetworkConnectedDevice,
103    url: &str,
104) -> Result<T, crate::Error> {
105    let client = reqwest::ClientBuilder::new()
106        .connect_timeout(std::time::Duration::from_secs(5))
107        .build()
108        .map_err(|err| crate::Error::Network(err))?;
109
110    let request = client
111        .request(reqwest::Method::GET, device.base_url() + url)
112        .build()
113        .map_err(|err| crate::Error::Network(err))?;
114    let response = client
115        .execute(request)
116        .await
117        .map_err(|err| crate::Error::Network(err))?;
118    let response = response
119        .text()
120        .await
121        .map_err(|err| crate::Error::Network(err))?;
122    serde_json::from_str::<T>(response.as_str())
123        .map_err(|err| crate::Error::InvalidJsonResponse(err))
124}
125impl super::Device for NetworkConnectedDevice {
126    async fn latest_measurement(
127        &self,
128    ) -> Result<Option<crate::measurements::Measurement>, crate::Error> {
129        fetch::<Option<Measurement>>(&self, "/api/v1/measurements/latest").await
130    }
131    async fn latest_successful_measurement(
132        &self,
133    ) -> Result<Option<crate::measurements::SuccessfulMeasurement>, crate::Error> {
134        fetch::<Option<SuccessfulMeasurement>>(&self, "/api/v1/measurements/latest_successful")
135            .await
136    }
137    async fn measurement_history(&self) -> Result<Vec<Measurement>, crate::Error> {
138        #[derive(Debug, Deserialize)]
139        struct MeasurementList {
140            pub values: Vec<Measurement>,
141        }
142        fetch::<MeasurementList>(&self, "/api/v1/measurements/history")
143            .await
144            .map(|l| l.values)
145    }
146    async fn system_status(&self) -> Result<SystemStatus, crate::Error> {
147        fetch::<SystemStatus>(&self, "/api/v1/system/status").await
148    }
149}