Accessing Protrack API (an Object-Oriented Approach)

Overview

Protrack is one of the well-known web-based GPS tracking software and today we will learn how to access its API using C# and .NET Framework. We will create a little wrapper around the API and use it to track GPS devices in a target account. This lesson will show you some of object-oriented concepts in action.

Requirements

A Protrack account with one or more GPS devices available for testing. If you are facing permission problems contact your vendor or Protrack technical support.

Introduction

Accessing Protrack API (https://www.protrack365.com/api.html) is fairly simple, it’s all about creating simple HTTP requests and handling the results which are represented in JSON. The trick here is using OOP concepts to make our code more reusable and more maintainable.

Dependencies

Start by adding a reference to Newtonsoft JSON.NET (https://www.newtonsoft.com/json) library to your project. This can be directly downloaded from vendor’s website or installed through NuGet Manager.ِ

Abstraction

Base Request

Referring to Protrack API we can see that all functions (except the authorization function) accept an access token as a parameter. This token is valid for certain time (2 hours) and it can be received through authorization only.

We will start by creating a base class that will represent the request data. The source code is fairly self-explanatory:

  internal abstract class ProtrackRequest {
    /// <summary>
    /// Path to function.
    /// </summary>
    public abstract string BaseUri { get; }
    /// <summary>
    /// This is used by all functions except 'authorization'.
    /// </summary>
    public string AccessToken { get; set; }

    /// <summary>
    /// Returns the list of request parameters packaged in a dictionary. Where Key is parameter name and Value is parameter value.
    /// </summary>
    public virtual IDictionary<string, object> GetParams() {
      var list = new Dictionary<string, object>();
      if (AccessToken != null) // adding access token only if necessary
        list.Add("access_token", AccessToken);
      return list;
    }

    /// <summary>
    /// Returns the list of request parameters as a query string.
    /// </summary>
    public virtual string GetParamsQueryString() {
      string queryString = string.Empty;

      foreach (var itm in GetParams()) {
        // This will keep empty parameters. You can skip them if you like.
        string valueStr = string.Empty;

        if (itm.Value != null)
          valueStr = System.Uri.EscapeDataString(itm.Value.ToString()); 

        queryString += string.Format("{0}={1}&", itm.Key, valueStr);
      }

      return queryString;
    }

    /// <summary>
    /// Returns full request signature (request URI along with parameter query string.)
    /// </summary>
    public virtual string GetRequestUri() {
      return BaseUri + "?" + GetParamsQueryString();
    }

    public override string ToString() {
      return GetRequestUri();
    }
  }

From the above code we can see that every request class derived from the base will have to fill its path (BaseUri property; mandatory) and parameter list (GetParams() method; optional).

Base Response

Referring to the Protrack API again we can see that every call response returned from the server besides being in JSON format, have two common attributes: code and message. The code may refer to one of the error codes available as a list in the API reference, while the message is the description. Keeping those two attributes in mind, we can create our response class:

  internal class ProtrackResponse {
    [JsonProperty("code")]
    public int Code { get; set; }

    [JsonIgnore]
    public ProtrackResponseCode ResponseCode { get { return (ProtrackResponseCode)Code; } }
    [JsonProperty("message")]
    public string Message { get; set; }
  }


  internal enum ProtrackResponseCode {
    Success = 0,
    SystemError = 10000,
    UnknownRequest = 10001,
    LoginTimeout = 10002,
    Unauthorized = 10003,
    ParameterError = 10004,
    MissingParameter = 10005,
    ParamOutOfRange = 10006,
    PermissionDenied = 10007,
    RequestLimit = 10009,
    AccessTokenNotExist = 10010,
    AccessTokenInvalid = 10011,
    AccessTokenExpired = 10012,
    ImeiUnauthorized = 10013,
    RequestTimeError = 10014,
    LoginFailed = 20001,
    TargetNotExist = 20005,
    DeviceOffline = 20017,
    SendCommandFailed = 20018,
    NoData = 20023,
    TargetExpired = 20046,
    Unsupported = 20048
  }

As response class need to be instantiated, we cannot just mark it as abstract. Abstract classes cannot be instantiated.

API Wrapper

Now the actual code that connects things together. This code represents the wrapper itself. The code is very generic. We will add function wrappers later.

  class ProtrackWrapper {
    protected string Account { get; set; }
    protected string Password { get; set; }
    /// <summary>
    /// API base URI
    /// </summary>
    protected string BaseUri { get { return "http://api.protrack365.com"; } }
    /// <summary>
    /// This will be used for all requests
    /// </summary>
    public string AccessToken { get; protected set; }
    /// <summary>
    /// Access token expiry date
    /// </summary>
    public DateTime? AccessTokenExpiresOnUtc { get; protected set; }


    public ProtrackWrapper(string account, string password) {
      this.Account = account;
      this.Password = password;
    }

    /// <summary>
    /// Returns a response from a web resource.
    /// </summary>
    /// <returns>Response represented as string.</returns>
    protected static string GetResponse(string requestUri) {
      HttpWebRequest req = WebRequest.CreateHttp(requestUri);
      using (var rsp = req.GetResponse())
      using (var stm = rsp.GetResponseStream())
      using (var rdr = new StreamReader(stm)) {
        return rdr.ReadToEnd();
      }
    }

    public virtual T GetResponse<T>(ProtrackRequest req) where T : ProtrackResponse {
      var requestUrl = new Uri(new Uri(BaseUri), req.GetRequestUri()).ToString();

      var rspStr = GetResponse(requestUrl);

      T rsp = JsonConvert.DeserializeObject<T>(rspStr);
      // should not throw a generic exception, 
      // and should not throw an exception for just everything
      // we will just keep this for now!
      if (rsp.Code != 0)
        throw new Exception(rsp.Message);

      return rsp;
    }
  }

As you can see in the above code, we used static polymorphism to create two versions of GetResponse(), one that returns bare response for bare request URI, and another one that accepts a typed request object and returned a typed response object. In fact, the other version is a generic one that returns only objects that derive from ProtrackResponse.

Authorization

To call any API function you need an access token, and this can be retrieved through the authorization function. The authorization function accepts three arguments: request time (in Unix format), username (i.e. account), and signature.

Unix Time

As described by Wikipedia, Unix time (also known as POSIX time or UNIX Epoch time) is a system for describing a point in time. It is the number of seconds that have elapsed since 00:00:00 Thursday, 1 January 1970, Coordinated Universal Time (UTC), minus leap seconds.

Unix time format will be used throughout the API, so we have created a helper class for it:

  static class UnixTimeHelper {
    /// <summary>
    /// Converts DateTime to Unix time.
    /// </summary>
    public static long ToUnixTime(this DateTime time) {
      var totalSeconds = (long)(time.Subtract(new DateTime(1970, 1, 1))).TotalSeconds;

      return totalSeconds;
    }
    /// <summary>
    /// Converts Unix time to DateTime.
    /// </summary>
    public static DateTime ToDateTime(long unixTime) {
      return new DateTime(1970, 1, 1).Add(TimeSpan.FromSeconds(unixTime));
    }
}

You do not have to worry about leap seconds as System.DateTime does not take leap seconds into account.

Signature

Signature is an MD5 hash of a combination of MD5 password hash and request time (in Unix format). In other words:

signature = MD5 ( MD5(password) + unix_time ) 

Signature is represented as 32 bytes lower-case string.

Authorization Request

The authorization request class is as follows:

  internal class ProtrackAuthorizationRequest : ProtrackRequest {
    /// <summary>
    /// Path to function.
    /// </summary>
    public override string BaseUri { get { return "api/authorization"; } }
    public string Account { get; protected set; }
    protected string Password { get; set; }
    public DateTime RequestTimeUtc { get; private set; }

    public ProtrackAuthorizationRequest() { }
    public ProtrackAuthorizationRequest(string account, string password) {
      this.Account = account;
      this.Password = password;
    }

    public override IDictionary<string, object> GetParams() {
      RequestTimeUtc = DateTime.UtcNow;
      var unixTime = UnixTimeHelper.ToUnixTime(RequestTimeUtc);

      string signature = GetSignature(unixTime);

      var list = base.GetParams(); // retrieving base parameters (if any)
      list.Add("time", unixTime);
      list.Add("account", this.Account);
      list.Add("signature", signature);

      return list;
    }

    private string GetSignature(long unixTime) {
      // signature is md5(md5(password) + time) encoded as a 32 bytes lower-case characters.
      var signature = ProtrackHelper.HashMD5(this.Password);
      signature = ProtrackHelper.HashMD5(signature + unixTime.ToString());
      return signature;
    }
  }

As you can see, you need to provide the function path through BaseUri. And by overriding GetParams() you can provide your parameter list.

To make things work, here’s the declaration of the MD5 hashing function:

  static class ProtrackHelper {
    public static string HashMD5(string input) {
      byte[] data = System.Text.Encoding.UTF8.GetBytes(input);
      data = System.Security.Cryptography.MD5.Create().ComputeHash(data);
      return BitConverter.ToString(data).Replace("-", "").ToLower();
    }
  }

Authorization Response

The authorization response class is fairly simple. It reflects the JSON response data returned from the server. While ProtrackAuthorizationResponse focuses on authorization attributes, the base ProtrackResponse has the two common attributes, code and message.

  internal class ProtrackAuthorizationResponse : ProtrackResponse {
    [JsonProperty("record")]
    public ProtrackAuthorizationRecord Record { get; set; }
  }

  internal class ProtrackAuthorizationRecord {
    [JsonProperty("access_token")]
    public string AccessToken { get; set; }
    [JsonProperty("expires_in")]
    public int ExpiresInSeconds { get; set; }
  }

We tagged properties with JsonPropertyAttribute attribute to allow our code to use different names for properties.

Connecting Things Together

Now we can add the following authorization code to the wrapper class:

    public void Authorize() {
      this.AccessToken = null;
      this.AccessTokenExpiresOnUtc = null;

      var req = new ProtrackAuthorizationRequest(this.Account, this.Password);
      var rsp = GetResponse<ProtrackAuthorizationResponse>(req);
      
      // updating access token and expiration time
      this.AccessToken = rsp.Record.AccessToken;
      this.AccessTokenExpiresOnUtc = req.RequestTimeUtc.AddSeconds(rsp.Record.ExpiresInSeconds);
    }

Now test your code and check if everything is going well:

      var wrapper = new ProtrackWrapper("test", "123456");
      wrapper.Authorize( );
      Console.WriteLine("Authorization code is: {0}", wrapper.AccessToken);.
      // Prints:
      // Authorization code is: A156321......69a0ef614ef3f582

Tracking

Now that everything is going well, we can move next to the tracking function. The tracking function accepts one or more GPS device server IMEI codes and returns latest coordinates for each code. Device server IMEI can be found through the Protrack web/mobile interface or through Param# command (specific GPS device models only.)

Track Request

Now that we have out requirements list create the request class:

  internal class ProtrackTrackRequest : ProtrackRequest {
    public override string BaseUri { get { return "api/track"; } }
    public string[] ImeiList { get; set; }

    public ProtrackTrackRequest() {

    }

    public ProtrackTrackRequest(string accessToken, string[] imeiList) {
      this.AccessToken = accessToken;
      this.ImeiList = imeiList;
    }

    public override IDictionary<string, object> GetParams() {
      var list = base.GetParams();
      list.Add("imeis", string.Join(",", ImeiList));

      return list;
    }
  }

Track Response

The response class lists the attributes that are returned from the server. I did not list all attributes, just for clarity.

  internal class ProtrackTrackResponse : ProtrackResponse {
    [JsonProperty("record")]
    public ProtrackTrackRecord[] Records { get; set; }
  }

  internal class ProtrackTrackRecord {
    [JsonProperty("imei")]
    public string IMEI { get; set; }
    [JsonProperty("longitude")]
    public decimal Longitude { get; set; }
    [JsonProperty("latitude")]
    public decimal Latitude { get; set; }
    [JsonProperty("systemtime")]
    public long SystemUnixTime { get; set; }
    [JsonProperty("gpstime")]
    public long GpsUnixTime { get; set; }

    // To make things easier, we have made extra DateTime properties
    // An alternative is to create a custom JSON converter for unix time
    public DateTime SystemTimeUtc { get { return UnixTimeHelper.ToDateTime(SystemUnixTime); } }
    public DateTime GpsTimeUtc { get { return UnixTimeHelper.ToDateTime(GpsUnixTime); } }

    // add any field you like
  } 

Connecting Things Together

Now add the following code to the wrapper class. Notice how we test access token expiration before making our request:

    public ProtrackTrackRecord Track(string imei) {
      return Track(new string[] { imei })[0];
    }
    public ProtrackTrackRecord[] Track(string[] imeiList) {
      if (this.AccessToken == null || DateTime.UtcNow >= this.AccessTokenExpiresOnUtc) {
        Authorize();
      }

      var req = new ProtrackTrackRequest(this.AccessToken, imeiList);
      var rsp = GetResponse<ProtrackTrackResponse>(req);

      return rsp.Records;
    }

And test:

      var track = wrapper.Track("123456789012345");
      Console.WriteLine("{0},{1},{2}", track.Latitude, track.Longitude, track.GpsTimeUtc);
      // Prints
      // 30.193456, 31.463092, 15 / 07 / 2019 19:41:38

One Step Further

In the previous response code as you can notice in those lines we have added two extra properties to convert Unix time to DateTime:

    public DateTime SystemTimeUtc { get { return UnixTimeHelper.ToDateTime(SystemUnixTime); } }
    public DateTime GpsTimeUtc { get { return UnixTimeHelper.ToDateTime(GpsUnixTime); } }

An alternative is to use a JSON converter:

  internal class JsonUnixTimeConverter : Newtonsoft.Json.Converters.DateTimeConverterBase {
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
      if (reader.TokenType != JsonToken.Integer)
        throw new Exception("Unexpected token type.");

      var unixTime = (long)reader.Value;

      return UnixTimeHelper.ToDateTime(unixTime);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
      if (false == value is DateTime)
        throw new Exception("Unexpected object type.");

      var dateTime = (DateTime)value;

      var unixTime = UnixTimeHelper.ToUnixTime(dateTime);

      writer.WriteValue(unixTime);
    }
  }

You will need to modify the ProtrackTrackRecord class:

    [JsonProperty("systemtime")]
    public DateTime SystemTimeUtc { get; set; }
    [JsonProperty("gpstime")]
    public DateTime GpsTimeUtc { get; set; }

And ProtrackWrapper. GetResponse<T> method:

      T rsp = JsonConvert.DeserializeObject<T>(rspStr, new JsonUnixTimeConverter());

Playback

Besides the access token, the playback method accepts single IMEI code, range start time and end time (both in Unix format.)

Playback Request

You can easily guess the request class code:

  internal class ProtrackPlaybackRequest : ProtrackRequest {
    public override string BaseUri { get { return "api/playback"; } }
    public string Imei { get; set; }
    public DateTime BeginTimeUtc{ get; set; }
    public DateTime EndTimeUtc { get; set; }

    public ProtrackPlaybackRequest() {

    }

    public ProtrackPlaybackRequest(string accessToken, string imei, DateTime beginTimeUtc, DateTime endTimeUtc) {
      this.AccessToken = accessToken;
      this.Imei = imei;
      this.BeginTimeUtc = beginTimeUtc;
      this.EndTimeUtc = endTimeUtc;
    }

    public override IDictionary<string, object> GetParams() {
      var list = base.GetParams();
      list.Add("imei", this.Imei);
      list.Add("begintime", UnixTimeHelper.ToUnixTime(BeginTimeUtc));
      list.Add("endtime", UnixTimeHelper.ToUnixTime(EndTimeUtc));

      return list;
    }
  }

Playback Response

The response class is fairly simple too:

  internal class ProtrackPlaybackResponse : ProtrackResponse {
    [JsonProperty("record")]
    public string RecordString { get; set; }

    // a custom JSON converter can be used here too
    public ProtrackPlaybackRecord[] GetRecords() {
      var recordsStrList = RecordString.Split(';');
      List<ProtrackPlaybackRecord> records = new List<ConsoleApp.ProtrackPlaybackRecord>(recordsStrList.Length);

      foreach (var recordStr in recordsStrList) {
        if (recordStr.Length == 0)
          continue;

        var record = new ProtrackPlaybackRecord(recordStr);
        records.Add(record);
      }

      return records.ToArray();
    }
  }

  internal class ProtrackPlaybackRecord {
    public decimal Longitude { get; set; }
    public decimal Latitude { get; set; }
    public DateTime GpsTimeUtc { get; set; }
    public int Speed { get; set; }
    public int Course { get; set; }

    public ProtrackPlaybackRecord() {

    }
    public ProtrackPlaybackRecord(string str) {
      string[] args = str.Split(',');

      Longitude = decimal.Parse(args[0]);
      Latitude = decimal.Parse(args[1]);
      GpsTimeUtc = UnixTimeHelper.ToDateTime(int.Parse(args[2]));
      Speed = int.Parse(args[3]);
      Course = int.Parse(args[4]);
    }
  }

Connecting Things Together

ProtrackWrapper code:

    public ProtrackPlaybackRecord[] Playback(string imei, DateTime beginTimeUtc, DateTime endTimeUtc) {
      if (this.AccessToken == null || DateTime.UtcNow >= this.AccessTokenExpiresOnUtc) {
        Authorize();
      }

      var req = new ProtrackPlaybackRequest(this.AccessToken, imei, beginTimeUtc, endTimeUtc);
      var rsp = GetResponse<ProtrackPlaybackResponse>(req);

      return rsp.GetRecords();
    }

And test:

      var records = wrapper.Playback("123456789012345", DateTime.UtcNow, DateTime.Today);
      foreach (var rec in records)
        Console.WriteLine("{0},{1},{2}", rec.GpsTimeUtc, rec.Latitude, rec.Longitude);

What’s Next

Using the above mentioned mechanism, you can easily create wrappers for the rest of API functions. I will be happy to receive your feedback and comments over this code.

Full Code Listing

Full code listing is available for download here: https://app.box.com/s/0nqvt7atf6hjueixk18qt23a9b3b5g7u