1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
///! TODO: doc
use crate::{
    measurements::{Measurement, SuccessfulMeasurement},
    SystemStatus,
};

/// UPW Sensor device connected via a network connection
pub struct NetworkConnectedDevice {
    ip_address: std::net::IpAddr,
    port: u16,
    use_tls: bool,
}

/// wrapper type for parameter TCP port. The default is port 80 (HTTP).
pub struct TcpPort(u16);
impl From<u16> for TcpPort {
    fn from(value: u16) -> Self {
        TcpPort(value)
    }
}
impl Default for TcpPort {
    fn default() -> Self {
        80.into()
    }
}

/// (mostly wrapper) type for parameter TLS usage. The default is [`UseTls::Auto`].
pub enum UseTls {
    /// do not use TLS
    Off,
    /// enforce TLS
    On,
    /// decide based on port number whether to use TLS
    Auto,
}
impl From<bool> for UseTls {
    fn from(value: bool) -> Self {
        if value {
            return UseTls::On;
        } else {
            return UseTls::Off;
        }
    }
}
impl Default for UseTls {
    fn default() -> Self {
        UseTls::Auto
    }
}

// constructors
pub fn connect_via_network(
    ip_address: std::net::IpAddr,
    use_tls: UseTls,
) -> NetworkConnectedDevice {
    connect_via_network_on_port(ip_address, 80.into(), use_tls)
}
pub fn connect_via_network_on_port(
    ip_address: std::net::IpAddr,
    port: TcpPort,
    use_tls: UseTls,
) -> NetworkConnectedDevice {
    NetworkConnectedDevice {
        ip_address,
        port: port.0,
        use_tls: match use_tls {
            UseTls::Off => false,
            UseTls::On => true,
            UseTls::Auto => port.0 == 443,
        },
    }
}

// implementation
impl NetworkConnectedDevice {
    /// returns the API base url without a path, e.g. `http://192.168.1.123:80`
    fn base_url(&self) -> String {
        format!(
            "{proto}://{addr}:{port}",
            proto = if self.use_tls { "https" } else { "http" },
            addr = self.ip_address.to_string(),
            port = self.port,
        )
    }
}
/// queries an API endpoint and deserializes the response
///
/// Examples:
///  ```rust,ignore
/// # use islabtech_upw_sensor_v1::{connect_via_network_on_port,Measurement};
/// # use islabtech_upw_sensor_v1::device::network::fetch;
/// let sensor = connect_via_network_on_port(
///     "192.168.1.123".parse().unwrap(),
///     80.into(),
///     Default::default(),
/// );
/// let result: Measurement = fetch::<Measurement>(&sensor, "/api/v1/measurements/latest").await?;
/// ```
async fn fetch<T: serde::de::DeserializeOwned>(
    device: &NetworkConnectedDevice,
    url: &str,
) -> Result<T, crate::Error> {
    let response = reqwest::get(device.base_url() + url)
        .await
        .map_err(|err| crate::Error::Network(err))?;
    let response = response
        .text()
        .await
        .map_err(|err| crate::Error::Network(err))?;
    serde_json::from_str::<T>(response.as_str())
        .map_err(|err| crate::Error::InvalidJsonResponse(err))
}
impl super::Device for NetworkConnectedDevice {
    async fn latest_measurement(&self) -> Result<crate::measurements::Measurement, crate::Error> {
        fetch::<Measurement>(&self, "/api/v1/measurements/latest").await
    }
    async fn latest_successful_measurement(
        &self,
    ) -> Result<Option<crate::measurements::SuccessfulMeasurement>, crate::Error> {
        fetch::<Option<SuccessfulMeasurement>>(&self, "/api/v1/measurements/latest_successful")
            .await
    }
    async fn system_status(&self) -> Result<SystemStatus, crate::Error> {
        fetch::<SystemStatus>(&self, "/api/v1/system/status").await
    }
}