yepzon-locationer/src/api.rs

313 lines
9.8 KiB
Rust

use super::Config;
use super::GenericError;
use chrono::format::ParseError;
use chrono::NaiveDateTime;
use minreq::Response;
use serde::Deserialize;
use std::fmt::{Display, Formatter};
use std::sync::mpsc;
use std::sync::mpsc::Receiver;
use std::thread;
use std::time::{Duration, Instant};
pub trait Timestamped {
fn timestamp(&self) -> Option<String>;
}
#[derive(Deserialize, Debug)]
pub struct ErrorModel {
#[serde(rename(deserialize = "Message"))]
pub message: Option<String>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct TagModel {
pub id: Option<String>,
pub imei: Option<String>,
#[serde(rename(deserialize = "batteryValue"))]
pub battery_value: Option<i32>,
#[serde(rename(deserialize = "btId"))]
pub bt_id: Option<String>,
#[serde(rename(deserialize = "nfcId"))]
pub nfc_id: Option<String>,
}
impl TagModel {
pub fn get_nick(&self, config: &Config) -> Option<String> {
if let Some(id) = &self.id {
if let Some(n) = config.nicknames.get(id) {
Some(n.clone())
} else {
None
}
} else {
None
}
}
pub fn get_id(&self) -> Result<String, GenericError> {
match &self.id {
Some(id) => Ok(id.to_string()),
None => Err(GenericError::MessagedError(
"Could not find device id. Error probably on Yetzon server side.".to_owned(),
None,
)),
}
}
}
impl Display for TagModel {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
let mut text = String::new();
text += &format!(
" id = {}",
self.id.as_ref().unwrap_or(&"unknown".to_owned())
);
text += &format!(
"\n imei = {}",
self.imei.as_ref().unwrap_or(&"unknown".to_owned())
);
text += &format!(
"\n battery value = {}%",
&self
.battery_value
.map(|v| v.to_string())
.unwrap_or("unknown".to_owned())
);
text += &format!(
"\n bluetooth id = {}",
&self.bt_id.as_ref().unwrap_or(&"unknown".to_owned())
);
text += &format!(
"\n NFC id = {}",
&self.nfc_id.as_ref().unwrap_or(&"unknown".to_owned())
);
write!(f, "{}", text)
}
}
#[derive(Deserialize, Debug, Clone)]
pub struct StateModel {
pub id: Option<String>,
pub timestamp: Option<String>,
#[serde(skip_deserializing)]
pub state: Option<String>,
#[serde(rename(deserialize = "realState"))]
pub real_state: Option<String>,
}
impl Timestamped for StateModel {
fn timestamp(&self) -> Option<String> {
self.timestamp.clone()
}
}
#[derive(Deserialize, Debug, Clone)]
#[serde(deny_unknown_fields)]
pub struct LocationModel {
pub timestamp: Option<String>,
#[serde(rename(deserialize = "type"))]
pub loc_type: Option<String>,
#[serde(rename(deserialize = "coordinateLat"))]
pub lat: Option<f64>,
#[serde(rename(deserialize = "coordinateLng"))]
pub lon: Option<f64>,
pub accuracy: Option<f32>,
}
impl Timestamped for LocationModel {
fn timestamp(&self) -> Option<String> {
self.timestamp.clone()
}
}
pub struct API {
pub config: Config,
location_req_que: Vec<String>,
}
impl API {
pub fn new(config: Config) -> API {
API {
config: config,
location_req_que: Vec::new(),
}
}
pub fn get_tags(&mut self) -> Result<Vec<TagModel>, GenericError> {
let response = self.request(self.config.tags_url.clone())?;
let tags = response.json();
if let Err(_) = tags {
let err: ErrorModel = response.json()?;
Err(GenericError::from(err))
} else {
tags.map_err(GenericError::from)
}
}
pub fn get_states(&mut self, tag_id: &String) -> Result<Vec<StateModel>, GenericError> {
let response = self.request(str::replace(&self.config.states_url, "{tag}", &tag_id))?;
Ok(response.json()?)
}
pub fn queue_location(&mut self, tag_id: &String, state_id: &String) {
let url = str::replace(&self.config.locations_url, "{tag}", &tag_id);
let url = str::replace(&url, "{state}", &state_id);
self.location_req_que.push(url);
}
pub fn begin_location_fetch(
&mut self,
) -> Receiver<Result<Result<Vec<LocationModel>, ErrorModel>, GenericError>> {
let (sender, receiver) = mpsc::channel();
let mut locations = self.location_req_que.clone();
let config = self.config.clone();
// sleep for a second to make sure no overlaps with previous requests.
thread::sleep(Duration::from_millis(1000));
thread::spawn(move || {
let (i_sender, i_receiver) = mpsc::channel();
let mut idx = 0;
let mut awaiting = 0;
let mut timer = 0;
let mut last_time = Instant::now();
let interval = 1_000_000_000 / config.throttle as u128;
loop {
let sender = sender.clone();
let config = config.clone();
let i_sender = i_sender.clone();
let now = Instant::now();
let delta = (now - last_time).as_nanos();
if timer > 0 {
timer = timer - delta.min(timer);
}
last_time = now;
if awaiting <= config.throttle && timer < (interval * (config.throttle as u128 - 2))
{
if locations.len() > idx {
let location = locations[idx].clone();
awaiting += 1;
thread::spawn(move || {
let response = minreq::get(location.clone())
.with_header("content-type", "application/json")
.with_header("x-api-key", &config.api_key)
.send()
.and_then(|r| match r.json() {
Ok(loc) => Ok(Ok(loc)),
Err(_) => Ok(Err(r.json()?)),
})
.map_err(GenericError::from);
i_sender.send((location, response)).ok();
});
timer += interval;
idx += 1;
} else {
while awaiting > 0 {
if let Ok((loc, rec)) = i_receiver.try_recv() {
API::add_error_back(loc, &rec, &mut locations);
awaiting -= 1;
sender.send(rec).ok();
}
}
if locations.len() <= idx {
break;
}
}
} else {
if let Ok((loc, rec)) = i_receiver.try_recv() {
API::add_error_back(loc, &rec, &mut locations);
awaiting -= 1;
sender.send(rec).ok();
}
}
}
});
self.location_req_que.clear();
receiver
}
pub fn get_between<T: Timestamped + Clone>(
list: &Vec<T>,
from: Option<NaiveDateTime>,
to: Option<NaiveDateTime>,
inclusive_from: bool,
config: &Config,
) -> Vec<(NaiveDateTime, T)> {
let mut timestamped = Vec::new();
for item in list {
if let Some(res) = API::parse_timestamp(&item.timestamp(), config) {
match res {
Ok(t) => {
if let Some(to) = to {
if t > to {
continue;
}
}
let mut too_old = false;
if let Some(from) = from {
if t < from {
too_old = true;
}
}
if too_old {
if inclusive_from {
timestamped.push((t.clone(), item.clone()));
}
break;
}
timestamped.push((t.clone(), item.clone()));
}
Err(e) => panic!(e),
}
} else {
println!("Skipped item, did not have timestmap");
}
}
timestamped
}
fn add_error_back(
location: String,
rec: &Result<Result<Vec<LocationModel>, ErrorModel>, GenericError>,
locations: &mut Vec<String>,
) -> bool {
if let Ok(inner) = rec {
if let Err(_) = inner {
locations.push(location);
true
} else {
false
}
} else {
false
}
}
fn request(&mut self, url: String) -> Result<Response, GenericError> {
let response = minreq::get(url)
.with_header("content-type", "application/json")
.with_header("x-api-key", &self.config.api_key)
.send();
response.map_err(GenericError::from)
}
fn parse_timestamp(
timestamp: &Option<String>,
config: &Config,
) -> Option<Result<NaiveDateTime, ParseError>> {
if let Some(timestamp) = &timestamp {
Some(NaiveDateTime::parse_from_str(
&timestamp,
&config.timestamp_format,
))
} else {
None
}
}
}