Separate commands, apistructs and add api command

This commit is contained in:
Sofia 2020-08-25 21:55:04 +03:00
parent 23cf2c370d
commit 762ea09b56
7 changed files with 556 additions and 277 deletions

View File

@ -1,125 +1,16 @@
pub mod structs;
use super::Config; use super::Config;
use super::GenericError; use super::GenericError;
use chrono::format::ParseError; use chrono::format::ParseError;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use minreq::Response; use minreq::Response;
use serde::Deserialize; use std::fmt::Display;
use std::fmt::{Display, Formatter};
use std::sync::mpsc; use std::sync::mpsc;
use std::sync::mpsc::Receiver; use std::sync::mpsc::Receiver;
use std::thread; use std::thread;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
pub use structs::*;
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)]
#[serde(deny_unknown_fields)]
pub struct StateModel {
pub id: Option<String>,
pub timestamp: Option<String>,
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 struct API {
pub config: Config, pub config: Config,
@ -134,8 +25,16 @@ impl API {
} }
} }
pub fn get_tags(&mut self) -> Result<Vec<TagModel>, GenericError> { #[allow(dead_code)]
let response = self.request(self.config.tags_url.clone())?; pub fn get_raw_string(&self, url: APIUrl) -> Result<String, GenericError> {
Ok(self
.request(API::api_url(url, &self.config))?
.as_str()?
.to_owned())
}
pub fn get_tags(&self) -> Result<Vec<TagModel>, GenericError> {
let response = self.request(API::api_url(APIUrl::Tags, &self.config))?;
let tags = response.json(); let tags = response.json();
if let Err(_) = tags { if let Err(_) = tags {
let err: ErrorModel = response.json()?; let err: ErrorModel = response.json()?;
@ -145,26 +44,39 @@ impl API {
} }
} }
pub fn get_states(&mut self, tag_id: &String) -> Result<Vec<StateModel>, GenericError> { pub fn get_states(&self, tag_id: &String) -> Result<Vec<StateModel>, GenericError> {
let response = self.request(str::replace(&self.config.states_url, "{tag}", &tag_id))?; let response = self.request(API::api_url(APIUrl::States(tag_id.clone()), &self.config))?;
Ok(response.json()?)
}
pub fn get_current_locations(
&self,
tag_id: &String,
) -> Result<Vec<LocationModel>, GenericError> {
let response = self.request(API::api_url(
APIUrl::CurrLocations(tag_id.clone()),
&self.config,
))?;
Ok(response.json()?) Ok(response.json()?)
} }
pub fn get_locations( pub fn get_locations(
&mut self, &self,
tag_id: &String, tag_id: &String,
state_id: &String, state_id: &String,
) -> Result<Vec<LocationModel>, GenericError> { ) -> Result<Vec<LocationModel>, GenericError> {
let url = str::replace(&self.config.locations_url, "{tag}", &tag_id); let response = self.request(API::api_url(
let url = str::replace(&url, "{state}", &state_id); APIUrl::Locations(tag_id.clone(), state_id.clone()),
let response = self.request(url)?; &self.config,
))?;
Ok(response.json()?) Ok(response.json()?)
} }
pub fn queue_location(&mut self, tag_id: &String, state_id: &String) { pub fn queue_location(&mut self, tag_id: &String, state_id: &String) {
let url = str::replace(&self.config.locations_url, "{tag}", &tag_id); self.location_req_que.push(API::api_url(
let url = str::replace(&url, "{state}", &state_id); APIUrl::Locations(tag_id.clone(), state_id.clone()),
self.location_req_que.push(url); &self.config,
));
} }
pub fn begin_location_fetch( pub fn begin_location_fetch(
@ -282,6 +194,35 @@ impl API {
timestamped timestamped
} }
pub fn print_list<T: Display + Timestamped>(list: Result<Vec<T>, GenericError>) {
match list {
Ok(items) => {
for item in items {
println!(
"\n{}\n{}",
item.timestamp().unwrap_or("No timestamp".to_owned()),
item
);
}
}
Err(e) => eprintln!("Could not complete request: {}", e),
}
}
pub fn api_url(url: APIUrl, config: &Config) -> String {
match url {
APIUrl::CurrLocations(tag) => {
str::replace(&config.current_locations_url, "{tag}", &tag)
}
APIUrl::Locations(tag, state) => {
let url = str::replace(&config.locations_url, "{tag}", &tag);
str::replace(&url, "{state}", &state)
}
APIUrl::States(tag) => str::replace(&config.states_url, "{tag}", &tag),
APIUrl::Tags => config.tags_url.clone(),
}
}
fn add_error_back( fn add_error_back(
location: String, location: String,
rec: &Result<Result<Vec<LocationModel>, ErrorModel>, GenericError>, rec: &Result<Result<Vec<LocationModel>, ErrorModel>, GenericError>,
@ -299,7 +240,7 @@ impl API {
} }
} }
fn request(&mut self, url: String) -> Result<Response, GenericError> { fn request(&self, url: String) -> Result<Response, GenericError> {
let response = minreq::get(url) let response = minreq::get(url)
.with_header("content-type", "application/json") .with_header("content-type", "application/json")
.with_header("x-api-key", &self.config.api_key) .with_header("x-api-key", &self.config.api_key)
@ -321,3 +262,10 @@ impl API {
} }
} }
} }
pub enum APIUrl {
CurrLocations(String),
Locations(String, String),
States(String),
Tags,
}

227
src/api/structs.rs Normal file
View File

@ -0,0 +1,227 @@
use super::{Config, GenericError};
use serde::Deserialize;
use std::fmt::{Display, Formatter};
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)]
#[serde(deny_unknown_fields)]
#[serde(rename_all = "camelCase")]
pub struct TagModel {
pub id: Option<String>,
pub imei: Option<String>,
pub battery_value: Option<i32>,
pub bt_id: Option<String>,
pub nfc_id: Option<String>,
pub pair_code: Option<String>,
pub location: Option<LocationModel>,
pub state: Option<ChangingState>,
pub last_contact: Option<String>,
pub charging: Option<bool>,
pub charger_connected: Option<bool>,
pub config: Option<ChangingConfig>,
pub capabilities: Option<Capabilities>,
}
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)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct ConfigModel {
pub active_interval: Option<i32>,
pub sleep_interval: Option<i32>,
pub sos_alert_sound_enabled: Option<bool>,
pub mode: Option<String>,
}
#[derive(Deserialize, Debug, Clone)]
#[serde(deny_unknown_fields)]
pub struct ChangingConfig {
current: Option<ConfigModel>,
pending: Option<ConfigModel>,
}
#[derive(Deserialize, Debug, Clone)]
#[serde(deny_unknown_fields)]
pub struct Capabilities {
rx: Option<bool>,
sos: Option<bool>,
}
#[derive(Deserialize, Debug, Clone)]
#[serde(deny_unknown_fields)]
#[serde(rename_all = "camelCase")]
pub struct StateModel {
pub id: Option<String>,
pub timestamp: Option<String>,
pub state: Option<String>,
pub real_state: Option<String>,
}
impl StateModel {
pub fn get_id(&self) -> Result<String, GenericError> {
match &self.id {
Some(id) => Ok(id.to_string()),
None => Err(GenericError::MessagedError(
"Could not find state id. Error probably on Yetzon server side.".to_owned(),
None,
)),
}
}
}
impl Timestamped for StateModel {
fn timestamp(&self) -> Option<String> {
self.timestamp.clone()
}
}
impl Display for StateModel {
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 timestamp = {}",
self.timestamp.as_ref().unwrap_or(&"unknown".to_owned())
);
text += &format!(
"\n state = {}",
self.state.as_ref().unwrap_or(&"unknown".to_owned())
);
text += &format!(
"\n real state = {}",
self.real_state
.as_ref()
.unwrap_or(&"no real state".to_owned())
);
write!(f, "{}", text)
}
}
#[derive(Deserialize, Debug, Clone)]
#[serde(deny_unknown_fields)]
pub struct ChangingState {
current: Option<StateModel>,
pending: Option<StateModel>,
}
#[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"))]
#[serde(alias = "latitude")]
pub lat: Option<f64>,
#[serde(rename(deserialize = "coordinateLng"))]
#[serde(alias = "longitude")]
pub lon: Option<f64>,
pub accuracy: Option<f32>,
}
impl Timestamped for LocationModel {
fn timestamp(&self) -> Option<String> {
self.timestamp.clone()
}
}
impl Display for LocationModel {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
let mut text = String::new();
text += &format!(
" timestamp = {}",
self.timestamp().as_ref().unwrap_or(&"unknown".to_owned())
);
text += &format!(
"\n location type = {}",
self.loc_type.as_ref().unwrap_or(&"unknown".to_owned())
);
text += &format!(
"\n latitude = {}",
self.lat
.as_ref()
.map(|v| v.to_string())
.unwrap_or("unknown".to_owned())
);
text += &format!(
"\n longitude = {}",
self.lon
.as_ref()
.map(|v| v.to_string())
.unwrap_or("unknown".to_owned())
);
text += &format!(
"\n accuracy = {}",
self.accuracy
.as_ref()
.map(|v| v.to_string())
.unwrap_or("unknown".to_owned())
);
write!(f, "{}", text)
}
}

View File

@ -20,6 +20,7 @@ pub struct EnvOpt {
pub enum Subcommand { pub enum Subcommand {
Between(BetweenOpt), Between(BetweenOpt),
Init(InitOpt), Init(InitOpt),
Api(ApiOpt),
Nick(NickOpt), Nick(NickOpt),
Get(GetOpt), Get(GetOpt),
} }
@ -58,6 +59,14 @@ pub struct InitOpt {
pub api_key: Option<String>, pub api_key: Option<String>,
} }
#[derive(FromArgs)]
#[argh(
subcommand,
name = "api",
description = "check integrity of API and API-key validity"
)]
pub struct ApiOpt {}
#[derive(FromArgs)] #[derive(FromArgs)]
#[argh( #[argh(
subcommand, subcommand,
@ -88,11 +97,11 @@ pub struct NickListOpt {}
#[argh( #[argh(
subcommand, subcommand,
name = "set", name = "set",
description = "Set nickname for given device index. see index with nick list" description = "Set nickname for given device. Give device index from nick list, or current nickname."
)] )]
pub struct NickSetOpt { pub struct NickSetOpt {
#[argh(positional, description = "device index, see index with nick list")] #[argh(positional, description = "the device index or nickname")]
pub index: i32, pub device: String,
#[argh(positional, description = "the new nickname for the device")] #[argh(positional, description = "the new nickname for the device")]
pub nickname: String, pub nickname: String,
} }
@ -111,10 +120,19 @@ pub struct GetOpt {
#[derive(FromArgs)] #[derive(FromArgs)]
#[argh(subcommand)] #[argh(subcommand)]
pub enum GetSub { pub enum GetSub {
Tags(GetTagsOpt),
States(GetStatesOpt), States(GetStatesOpt),
Locations(GetLocationsOpt), Locations(GetLocationsOpt),
} }
#[derive(FromArgs)]
#[argh(
subcommand,
name = "tags",
description = "Get all tags connected to this API-key"
)]
pub struct GetTagsOpt {}
#[derive(FromArgs)] #[derive(FromArgs)]
#[argh( #[argh(
subcommand, subcommand,
@ -136,5 +154,5 @@ pub struct GetLocationsOpt {
#[argh(positional, description = "the device in question")] #[argh(positional, description = "the device in question")]
pub device: String, pub device: String,
#[argh(positional, description = "the state id")] #[argh(positional, description = "the state id")]
pub state: String, pub state: Option<String>,
} }

194
src/commands.rs Normal file
View File

@ -0,0 +1,194 @@
use super::api::{LocationModel, StateModel, TagModel, API};
use super::config::Config;
use super::errors::GenericError;
use super::gpx;
use chrono::NaiveDateTime;
use std::io::prelude::*;
use std::path::PathBuf;
use std::sync::mpsc::TryRecvError;
use std::time::Duration;
pub fn init(path: &PathBuf, api_key: Option<String>) -> Result<(), GenericError> {
let mut config = Config::default();
if let Some(api_key) = api_key {
config.api_key = api_key;
}
Ok(config.write_to(path)?)
}
pub fn between(
api: &mut API,
tag_str: String,
from: Option<NaiveDateTime>,
to: Option<NaiveDateTime>,
) -> Result<(TagModel, Vec<(NaiveDateTime, LocationModel)>), GenericError> {
let tags = api.get_tags()?;
let tag = find_tag(tag_str, &tags, &api.config)?;
let tag_id = tag.get_id()?;
print!("Preparing..\r");
std::io::stdout().lock().flush().ok();
let state_list = api.get_states(&tag_id)?;
let states = API::get_between(&state_list, from, to, true, &api.config);
let mut locations = Vec::new();
for (_, state) in states.iter() {
api.queue_location(&tag_id, state.id.as_ref().unwrap());
}
let receiver = api.begin_location_fetch();
let mut counter = 0;
loop {
match receiver.try_recv() {
Ok(res) => match res {
Ok(loc) => match loc {
Ok(mut loc) => {
counter += 1;
let remaining = exp_time(&api, states.len() as u64 - counter as u64);
print!(
"Done: {:<5.2}% Remaining: {:<5.2} seconds\r",
counter as f32 / states.len() as f32 * 100.,
remaining.as_secs_f32()
);
std::io::stdout().lock().flush().ok();
locations.append(&mut loc);
}
Err(e) => eprintln!(
"Error fetching location data: {}",
e.message.unwrap_or(String::new())
),
},
Err(e) => eprintln!("{}", e),
},
Err(e) => {
if let TryRecvError::Disconnected = e {
break;
}
}
}
}
println!("{:<100}", "\nDone!");
locations.sort_by(|loc1, loc2| loc2.timestamp.cmp(&loc1.timestamp));
let locs = API::get_between(&locations, from, to, false, &api.config);
let gpx = gpx::generate_gpx(&tag, &locs, &api.config)?;
gpx::write_gpx(&gpx)?;
Ok((tag, locs))
}
pub fn check_api(api: &API) -> Result<(), GenericError> {
let tags = api.get_tags()?;
if let Some(tag) = tags.get(0) {
let tag_id = tag.get_id()?;
api.get_current_locations(&tag_id)?;
if let Some(state) = api.get_states(&tag_id)?.get(0) {
let state_id = state.get_id()?;
api.get_locations(&tag_id, &state_id)?;
Ok(())
} else {
Err(GenericError::MessagedError(
"Could not find any states for the first device found.".to_owned(),
None,
))
}
} else {
Err(GenericError::MessagedError("Could not find any devices. Please make sure this API-key is paired with a device first.".to_owned(), None))
}
}
pub fn nick_list(api: &API) -> Result<Vec<((usize, String), TagModel)>, GenericError> {
let mut tags = api.get_tags()?;
tags.sort_by(|tag1, tag2| tag1.id.cmp(&tag2.id));
let list = tags
.iter()
.enumerate()
.map(|(idx, tag)| {
(
(
idx + 1,
tag.get_nick(&api.config)
.unwrap_or("No current nick".to_owned()),
),
tag.clone(),
)
})
.collect();
Ok(list)
}
pub fn nick_set(
api: &mut API,
config_path: &PathBuf,
tag_str: String,
nickname: String,
) -> Result<(), GenericError> {
let mut tags = api.get_tags()?;
tags.sort_by(|tag1, tag2| tag1.id.cmp(&tag2.id));
let tag = find_tag(tag_str.clone(), &tags, &api.config)?;
if let Some(id) = &tag.id {
api.config.nicknames.insert(id.clone(), nickname);
api.config.write_to(config_path)?;
Ok(())
} else {
Err(GenericError::MessagedError(
format!("Device {} does not have an id", tag_str),
None,
))
}
}
pub fn get_locations(
api: &API,
tag: String,
state: Option<String>,
) -> Result<Vec<LocationModel>, GenericError> {
let tags = api.get_tags()?;
let tag = find_tag(tag, &tags, &api.config)?;
if let Some(state) = state {
Ok(api.get_locations(&tag.get_id()?, &state)?)
} else {
Ok(api.get_current_locations(&tag.get_id()?)?)
}
}
pub fn get_states(api: &API, tag: String) -> Result<Vec<StateModel>, GenericError> {
let tags = api.get_tags()?;
let tag = find_tag(tag, &tags, &api.config)?;
Ok(api.get_states(&tag.get_id()?)?)
}
fn exp_time(api: &API, reqs_left: u64) -> Duration {
let interval = 1_000 / api.config.throttle as u64;
Duration::from_millis(interval * reqs_left)
}
fn find_tag(
tag_str: String,
tags: &Vec<TagModel>,
config: &Config,
) -> Result<TagModel, GenericError> {
let mut tag = None;
if let Ok(num) = tag_str.parse::<i32>() {
if let Some(found) = tags.get((num - 1).max(0) as usize) {
tag = Some(found.clone());
}
}
for curr in tags.iter() {
if let Some(nick) = curr.get_nick(config) {
if nick == tag_str {
tag = Some(curr.clone());
}
}
}
match tag {
Some(tag) => Ok(tag),
None => Err(GenericError::MessagedError(
format!("Could not find device {}", tag_str),
None,
)),
}
}

View File

@ -12,6 +12,7 @@ pub struct Config {
pub tags_url: String, pub tags_url: String,
pub states_url: String, pub states_url: String,
pub locations_url: String, pub locations_url: String,
pub current_locations_url: String,
pub timestamp_format: String, pub timestamp_format: String,
pub timedate_format: String, pub timedate_format: String,
@ -50,6 +51,7 @@ impl Default for Config {
tags_url: "https://platform.yepzon.com/tags".to_owned(), tags_url: "https://platform.yepzon.com/tags".to_owned(),
states_url: "https://platform.yepzon.com/tags/{tag}/states".to_owned(), states_url: "https://platform.yepzon.com/tags/{tag}/states".to_owned(),
locations_url: "https://platform.yepzon.com/tags/{tag}/locations/{state}".to_owned(), locations_url: "https://platform.yepzon.com/tags/{tag}/locations/{state}".to_owned(),
current_locations_url: "https://platform.yepzon.com/tags/{tag}/locations".to_owned(),
timestamp_format: "%Y-%m-%dT%H:%M:%S%.fZ".to_owned(), timestamp_format: "%Y-%m-%dT%H:%M:%S%.fZ".to_owned(),

View File

@ -1,13 +1,13 @@
use super::{Config, GenericError, LocationModel, TagModel}; use super::{Config, GenericError, LocationModel, TagModel};
use chrono::offset::{LocalResult, TimeZone, Utc as UTC_ZONE}; use chrono::offset::Utc as UTC_ZONE;
use chrono::{DateTime, NaiveDateTime, Utc}; use chrono::{DateTime, NaiveDateTime, Utc};
use geo_types::Point; use geo_types::Point;
use gpx::{Gpx, GpxVersion, Metadata, Person, Route, Track, TrackSegment, Waypoint}; use gpx::{Gpx, GpxVersion, Metadata, Person, Route, Track, TrackSegment, Waypoint};
use std::fs::File; use std::fs::File;
pub fn generate_gpx( pub fn generate_gpx(
tag: TagModel, tag: &TagModel,
locations: Vec<(NaiveDateTime, LocationModel)>, locations: &Vec<(NaiveDateTime, LocationModel)>,
config: &Config, config: &Config,
) -> Result<Gpx, GenericError> { ) -> Result<Gpx, GenericError> {
let route = Route::default(); let route = Route::default();
@ -47,14 +47,14 @@ pub fn write_gpx(gpx: &Gpx) -> Result<(), GenericError> {
Ok(()) Ok(())
} }
fn get_waypoint(point: (NaiveDateTime, LocationModel)) -> Result<Waypoint, GenericError> { fn get_waypoint(point: &(NaiveDateTime, LocationModel)) -> Result<Waypoint, GenericError> {
let (naive_time, location) = point; let (naive_time, location) = point;
let now = Utc::now(); let now = Utc::now();
let time: DateTime<UTC_ZONE> = DateTime::from_utc(naive_time, now.offset().clone()); let time: DateTime<UTC_ZONE> = DateTime::from_utc(*naive_time, now.offset().clone());
if let (Some(lat), Some(lon)) = (location.lat, location.lon) { if let (Some(lat), Some(lon)) = (location.lat, location.lon) {
let mut waypoint = Waypoint::new(Point::new(lon, lat)); let mut waypoint = Waypoint::new(Point::new(lon, lat));
waypoint.time = Some(time); waypoint.time = Some(time);
waypoint.source = location.loc_type; waypoint.source = location.loc_type.as_ref().cloned();
Ok(waypoint) Ok(waypoint)
} else { } else {
Err(GenericError::MessagedError( Err(GenericError::MessagedError(

View File

@ -1,18 +1,16 @@
mod api; mod api;
mod cmd; mod args;
mod commands;
mod config; mod config;
mod errors; mod errors;
mod gpx; mod gpx;
use api::{LocationModel, TagModel, API}; use api::{LocationModel, TagModel, API};
use args::*;
use chrono::offset::Local; use chrono::offset::Local;
use chrono::{NaiveDate, NaiveDateTime, NaiveTime, ParseError}; use chrono::{NaiveDate, NaiveDateTime, NaiveTime, ParseError};
use cmd::*;
use config::Config; use config::Config;
use errors::{GenericError, MessagedError}; use errors::{GenericError, MessagedError};
use std::io::prelude::*;
use std::sync::mpsc::TryRecvError;
use std::time::Duration;
fn main() { fn main() {
let env: EnvOpt = argh::from_env(); let env: EnvOpt = argh::from_env();
@ -26,6 +24,7 @@ fn from_env(env: EnvOpt) -> Result<(), GenericError> {
match env.subcommand { match env.subcommand {
Subcommand::Between(opt) => { Subcommand::Between(opt) => {
let config = Config::from_path(&env.config)?; let config = Config::from_path(&env.config)?;
let mut api = API::new(config.clone());
let since = let since =
Some(try_get_datetime(opt.since, &config).with_msg("Failed to parse since")?); Some(try_get_datetime(opt.since, &config).with_msg("Failed to parse since")?);
@ -37,76 +36,60 @@ fn from_env(env: EnvOpt) -> Result<(), GenericError> {
None => None, None => None,
}; };
let (tag, locs) = run(&config, opt.device, since, until)?; let (_, locs) = commands::between(&mut api, opt.device, since, until)?;
dbg!( dbg!(
&locs.iter().map(|loc| loc.0).collect::<Vec<NaiveDateTime>>(), &locs.iter().map(|loc| loc.0).collect::<Vec<NaiveDateTime>>(),
locs.len(), locs.len(),
); );
let gpx = gpx::generate_gpx(tag, locs, &config)?;
gpx::write_gpx(&gpx)?;
Ok(()) Ok(())
} }
Subcommand::Init(opt) => { Subcommand::Init(opt) => {
let mut config = Config::default(); commands::init(&env.config, opt.api_key)?;
if let Some(api_key) = opt.api_key {
config.api_key = api_key;
}
config.write_to(&env.config)?;
Ok(()) Ok(())
} }
Subcommand::Api(_) => {
let config = Config::from_path(&env.config)?;
let api = API::new(config.clone());
match commands::check_api(&api) {
Ok(_) => {
println!("API verified, no issues");
Ok(())
}
Err(e) => Err(GenericError::MessagedError(
"API integrity failed, or API-key is not valid. ".to_owned(),
Some(Box::new(e)),
)),
}
}
Subcommand::Nick(opt) => { Subcommand::Nick(opt) => {
let mut config = Config::from_path(&env.config)?; let config = Config::from_path(&env.config)?;
let mut api = API::new(config.clone()); let mut api = API::new(config.clone());
let mut tags = api.get_tags()?;
tags.sort_by(|tag1, tag2| tag1.id.cmp(&tag2.id));
match opt.subcommand { match opt.subcommand {
NickSub::List(_) => { NickSub::List(_) => {
for (idx, tag) in tags.iter().enumerate() { for ((idx, nick), tag) in commands::nick_list(&api)? {
println!( println!("{}. {}\n{}", idx, nick, tag);
"{}. {}\n{}",
idx + 1,
tag.get_nick(&config)
.unwrap_or("No current nick".to_owned()),
tag
);
} }
Ok(())
} }
NickSub::Set(opt) => { NickSub::Set(opt) => {
if let Some(tag) = tags.get((opt.index - 1).max(0) as usize) { commands::nick_set(&mut api, &env.config, opt.device, opt.nickname)?;
if let Some(id) = &tag.id {
config.nicknames.insert(id.clone(), opt.nickname);
config.write_to(&env.config)?;
Ok(())
} else {
Err(GenericError::MessagedError(
format!("Device with index {} does not have an id", opt.index),
None,
))
}
} else {
Err(GenericError::MessagedError(
format!("Could not find device with index {}", opt.index),
None,
))
}
} }
} }
Ok(())
} }
Subcommand::Get(opt) => { Subcommand::Get(opt) => {
let config = Config::from_path(&env.config)?; let config = Config::from_path(&env.config)?;
let mut api = API::new(config.clone()); let api = API::new(config.clone());
let tags = api.get_tags()?;
match opt.subcommand { match opt.subcommand {
GetSub::Tags(_) => {
let tags = api.get_tags()?;
dbg!(&tags);
}
GetSub::States(opt) => { GetSub::States(opt) => {
let tag = find_tag(opt.device, &tags, &config)?; API::print_list(commands::get_states(&api, opt.device));
dbg!(api.get_states(&tag.get_id()?));
} }
GetSub::Locations(opt) => { GetSub::Locations(opt) => {
let tag = find_tag(opt.device, &tags, &config)?; API::print_list(commands::get_locations(&api, opt.device, opt.state))
dbg!(api.get_locations(&tag.get_id()?, &opt.state));
} }
} }
Ok(()) Ok(())
@ -114,72 +97,6 @@ fn from_env(env: EnvOpt) -> Result<(), GenericError> {
} }
} }
fn run(
config: &Config,
tag_str: String,
from: Option<NaiveDateTime>,
to: Option<NaiveDateTime>,
) -> Result<(TagModel, Vec<(NaiveDateTime, LocationModel)>), GenericError> {
let mut api = API::new(config.clone());
let tags = api.get_tags()?;
let tag = find_tag(tag_str, &tags, config)?;
let tag_id = tag.get_id()?;
print!("Preparing..\r");
std::io::stdout().lock().flush().ok();
let state_list = api.get_states(&tag_id)?;
let states = API::get_between(&state_list, from, to, true, &config);
let mut locations = Vec::new();
for (_, state) in states.iter() {
api.queue_location(&tag_id, state.id.as_ref().unwrap());
}
let receiver = api.begin_location_fetch();
let mut counter = 0;
loop {
match receiver.try_recv() {
Ok(res) => match res {
Ok(loc) => match loc {
Ok(mut loc) => {
counter += 1;
let remaining = exp_time(&api, states.len() as u64 - counter as u64);
print!(
"Done: {:<5.2}% Remaining: {:<5.2} seconds\r",
counter as f32 / states.len() as f32 * 100.,
remaining.as_secs_f32()
);
std::io::stdout().lock().flush().ok();
locations.append(&mut loc);
}
Err(e) => eprintln!(
"Error fetching location data: {}",
e.message.unwrap_or(String::new())
),
},
Err(e) => eprintln!("{}", e),
},
Err(e) => {
if let TryRecvError::Disconnected = e {
break;
}
}
}
}
println!("{:<100}", "\nDone!");
locations.sort_by(|loc1, loc2| loc2.timestamp.cmp(&loc1.timestamp));
let locs = API::get_between(&locations, from, to, false, &config);
Ok((tag, locs))
}
fn exp_time(api: &API, reqs_left: u64) -> Duration {
let interval = 1_000 / api.config.throttle as u64;
Duration::from_millis(interval * reqs_left)
}
fn try_get_datetime(parsed: String, config: &Config) -> Result<NaiveDateTime, ParseError> { fn try_get_datetime(parsed: String, config: &Config) -> Result<NaiveDateTime, ParseError> {
match NaiveDateTime::parse_from_str(&parsed, &config.timedate_format) { match NaiveDateTime::parse_from_str(&parsed, &config.timedate_format) {
Ok(timedate) => Ok(timedate), Ok(timedate) => Ok(timedate),
@ -192,30 +109,3 @@ fn try_get_datetime(parsed: String, config: &Config) -> Result<NaiveDateTime, Pa
}, },
} }
} }
fn find_tag(
tag_str: String,
tags: &Vec<TagModel>,
config: &Config,
) -> Result<TagModel, GenericError> {
let mut tag = None;
if let Ok(num) = tag_str.parse::<i32>() {
if let Some(found) = tags.get((num - 1).max(0) as usize) {
tag = Some(found.clone());
}
}
for curr in tags.iter() {
if let Some(nick) = curr.get_nick(config) {
if nick == tag_str {
tag = Some(curr.clone());
}
}
}
match tag {
Some(tag) => Ok(tag),
None => Err(GenericError::MessagedError(
format!("Could not find device {}", tag_str),
None,
)),
}
}