mirror of
https://github.com/q39JzrRa/GM-Vehicle-API.git
synced 2026-05-19 11:54:59 -04:00
Converted to base class and implementation for command methods
client now reqires an active vehicle object pin is no longer a parameter - upgrade must be called by app More code comments and cleanup More normalized object models diagnostic command implemented in test app
This commit is contained in:
@@ -1,533 +0,0 @@
|
||||
using GM.Api.Models;
|
||||
using GM.Api.Tokens;
|
||||
using JWT;
|
||||
using JWT.Algorithms;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GM.Api
|
||||
{
|
||||
public class GMClient
|
||||
{
|
||||
public static int RetryCount { get; set; } = 3;
|
||||
|
||||
//TODO: consistent exception throwing
|
||||
|
||||
string _clientId;
|
||||
string _deviceId;
|
||||
JwtTool _jwtTool;
|
||||
string _apiUrl;
|
||||
string _host;
|
||||
|
||||
HttpClient _client;
|
||||
|
||||
bool _isUpgraded = false;
|
||||
bool _isConnected = false;
|
||||
|
||||
public LoginData LoginData { get; set; } = null;
|
||||
|
||||
public Func<LoginData, Task> TokenUpdateCallback { get; set; }
|
||||
|
||||
|
||||
public GMClient(GmConfiguration config, string brand, string deviceId)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public GMClient(string clientId, string deviceId, string clientSecret, string apiUrl)
|
||||
{
|
||||
Setup(clientId, deviceId, clientSecret, apiUrl);
|
||||
}
|
||||
|
||||
void Setup(string clientId, string deviceId, string clientSecret, string apiUrl)
|
||||
{
|
||||
_clientId = clientId;
|
||||
_deviceId = deviceId;
|
||||
_jwtTool = new JwtTool(clientSecret);
|
||||
_apiUrl = apiUrl;
|
||||
var uri = new Uri(_apiUrl);
|
||||
_host = uri.Host;
|
||||
_client = CreateClient(_host);
|
||||
}
|
||||
|
||||
|
||||
static HttpClient CreateClient(string host)
|
||||
{
|
||||
var client = new HttpClient(new HttpClientHandler() { AllowAutoRedirect = true, AutomaticDecompression = System.Net.DecompressionMethods.GZip });
|
||||
|
||||
client.DefaultRequestHeaders.AcceptEncoding.SetValue("gzip");
|
||||
client.DefaultRequestHeaders.Accept.SetValue("application/json");
|
||||
client.DefaultRequestHeaders.AcceptLanguage.SetValue("en-US");
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd("okhttp/3.9.0");
|
||||
client.DefaultRequestHeaders.Host = host;
|
||||
client.DefaultRequestHeaders.MaxForwards = 10;
|
||||
client.DefaultRequestHeaders.ExpectContinue = false;
|
||||
return client;
|
||||
}
|
||||
|
||||
|
||||
async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool noAuth = false)
|
||||
{
|
||||
if (!noAuth)
|
||||
{
|
||||
if (LoginData == null)
|
||||
{
|
||||
throw new InvalidOperationException("Not Logged in");
|
||||
}
|
||||
if (LoginData.IsExpired)
|
||||
{
|
||||
var result = await RefreshToken();
|
||||
if (!result)
|
||||
{
|
||||
throw new InvalidOperationException("Token refresh failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
request.Headers.Authorization = null;
|
||||
}
|
||||
|
||||
int attempt = 0;
|
||||
while (attempt < RetryCount)
|
||||
{
|
||||
attempt++;
|
||||
HttpResponseMessage resp = null;
|
||||
try
|
||||
{
|
||||
resp = await _client.SendAsync(request);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//todo: only catch transient errors
|
||||
//todo: log this
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized || resp.StatusCode == System.Net.HttpStatusCode.Forbidden)
|
||||
{
|
||||
var result = await RefreshToken();
|
||||
if (!result)
|
||||
{
|
||||
throw new InvalidOperationException("Token refresh failed");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
else if (resp.StatusCode == System.Net.HttpStatusCode.BadGateway || resp.StatusCode == System.Net.HttpStatusCode.Conflict || resp.StatusCode == System.Net.HttpStatusCode.GatewayTimeout || resp.StatusCode == System.Net.HttpStatusCode.InternalServerError || resp.StatusCode == System.Net.HttpStatusCode.RequestTimeout || resp.StatusCode == System.Net.HttpStatusCode.ResetContent || resp.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)
|
||||
{
|
||||
//possible transient errors
|
||||
//todo: log this
|
||||
await Task.Delay(500);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
var respMessage = (await resp.Content.ReadAsStringAsync())??"";
|
||||
throw new InvalidOperationException("Request error. StatusCode: " + resp.StatusCode.ToString() + ", msg: " + respMessage);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
//todo: include more info
|
||||
throw new InvalidOperationException("Request failed too many times");
|
||||
}
|
||||
|
||||
|
||||
|
||||
async Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content, bool noAuth = false)
|
||||
{
|
||||
return await SendAsync(new HttpRequestMessage(HttpMethod.Post, requestUri) { Content = content }, noAuth);
|
||||
}
|
||||
|
||||
async Task<HttpResponseMessage> GetAsync(string requestUri, bool noAuth = false)
|
||||
{
|
||||
return await SendAsync(new HttpRequestMessage(HttpMethod.Get, requestUri), noAuth);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async Task<Commandresponse> VehicleConnect(string vin)
|
||||
{
|
||||
var response = await PostAsync($"{_apiUrl}/v1/account/vehicles/{vin}/commands/connect", new StringContent("{}", Encoding.UTF8, "application/json"));
|
||||
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var respString = await response.Content.ReadAsStringAsync();
|
||||
var respObj = JsonConvert.DeserializeObject<CommandResponseRoot>(respString);
|
||||
if (respObj == null || respObj.commandResponse == null) return null;
|
||||
return respObj.commandResponse;
|
||||
}
|
||||
else
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
async Task<bool> UpgradeToken(string pin)
|
||||
{
|
||||
var payload = new LoginRequest()
|
||||
{
|
||||
ClientId = _clientId,
|
||||
DeviceId = _deviceId,
|
||||
Credential = pin,
|
||||
CredentialType = "PIN",
|
||||
Nonce = helpers.GenerateNonce(),
|
||||
Timestamp = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK")
|
||||
};
|
||||
|
||||
var token = _jwtTool.EncodeToken(payload);
|
||||
|
||||
var response = await PostAsync($"{_apiUrl}/v1/oauth/token/upgrade", new StringContent(token, Encoding.UTF8, "text/plain"));
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
_isUpgraded = true;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> Login(string username, string password)
|
||||
{
|
||||
var payload = new LoginRequest()
|
||||
{
|
||||
ClientId = _clientId,
|
||||
DeviceId = _deviceId,
|
||||
GrantType = "password",
|
||||
Nonce = helpers.GenerateNonce(),
|
||||
Password = password,
|
||||
Scope = "onstar gmoc commerce user_trailer msso",
|
||||
Timestamp = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK"),
|
||||
Username = username
|
||||
};
|
||||
|
||||
var token = _jwtTool.EncodeToken(payload);
|
||||
|
||||
var response = await PostAsync($"{_apiUrl}/v1/oauth/token", new StringContent(token, Encoding.UTF8, "text/plain"), true);
|
||||
|
||||
|
||||
string rawResponseToken = null;
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
rawResponseToken = await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(rawResponseToken))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var loginTokenData = _jwtTool.DecodeTokenToObject<LoginData>(rawResponseToken);
|
||||
|
||||
LoginData = loginTokenData;
|
||||
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", LoginData.AccessToken);
|
||||
|
||||
//todo: should this be a copy rather than a reference?
|
||||
await TokenUpdateCallback?.Invoke(LoginData);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> RefreshToken()
|
||||
{
|
||||
if (LoginData == null) return false;
|
||||
|
||||
var payload = new LoginRequest()
|
||||
{
|
||||
ClientId = _clientId,
|
||||
DeviceId = _deviceId,
|
||||
GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
||||
Nonce = helpers.GenerateNonce(),
|
||||
Scope = "onstar gmoc commerce user_trailer",
|
||||
Timestamp = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK"),
|
||||
Assertion = LoginData.IdToken
|
||||
};
|
||||
|
||||
var token = _jwtTool.EncodeToken(payload);
|
||||
|
||||
var response = await PostAsync($"{_apiUrl}/v1/oauth/token", new StringContent(token, Encoding.UTF8, "text/plain"), true);
|
||||
|
||||
string rawResponseToken = null;
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
rawResponseToken = await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(rawResponseToken))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/*{
|
||||
"access_token": ,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 1800,
|
||||
"scope": "user_trailer onstar commerce gmoc role_owner",
|
||||
"user_info": {
|
||||
"RemoteUserId": "",
|
||||
"country": ""
|
||||
}
|
||||
}*/
|
||||
// Not sure if the scope needs to be updated, as msso has been removed in the refresh request
|
||||
|
||||
var refreshData = _jwtTool.DecodeTokenToObject<LoginData>(rawResponseToken);
|
||||
|
||||
LoginData.AccessToken = refreshData.AccessToken;
|
||||
LoginData.IssuedAtUtc = refreshData.IssuedAtUtc;
|
||||
LoginData.ExpiresIn = refreshData.ExpiresIn;
|
||||
|
||||
//should we assume the upgrade status is broken?
|
||||
|
||||
|
||||
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", LoginData.AccessToken);
|
||||
|
||||
//todo: should this be a copy rather than a reference?
|
||||
await TokenUpdateCallback?.Invoke(LoginData);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#region Commands
|
||||
|
||||
|
||||
|
||||
public async Task<Commandresponse> InitiateCommand(string vin, string pin, string command)
|
||||
{
|
||||
if (!_isConnected)
|
||||
{
|
||||
await VehicleConnect(vin);
|
||||
_isConnected = true;
|
||||
}
|
||||
|
||||
await Task.Delay(500);
|
||||
|
||||
if (!_isUpgraded)
|
||||
{
|
||||
if (!await UpgradeToken(pin)) return null;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
|
||||
|
||||
JObject reqObj;
|
||||
|
||||
if (command == "lockDoor" || command == "unlockDoor")
|
||||
{
|
||||
reqObj = new JObject()
|
||||
{
|
||||
[$"{command}Request"] = new JObject()
|
||||
{
|
||||
["delay"] = 0
|
||||
}
|
||||
};
|
||||
}
|
||||
else if (command == "alert")
|
||||
{
|
||||
reqObj = new JObject()
|
||||
{
|
||||
//TODO: these parameters may be controllable :D
|
||||
[$"{command}Request"] = new JObject()
|
||||
{
|
||||
["action"] = new JArray() { "Honk", "Flash" },
|
||||
["delay"] = 0,
|
||||
["duration"] = 1,
|
||||
["override"] = new JArray() { "DoorOpen", "IgnitionOn" }
|
||||
}
|
||||
};
|
||||
}
|
||||
else if (command == "diagnostics")
|
||||
{
|
||||
reqObj = new JObject()
|
||||
{
|
||||
[$"{command}Request"] = new JObject()
|
||||
{
|
||||
["diagnosticItem"] = new JArray(DiagnosticRequestRoot.DefaultItems)
|
||||
}
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
reqObj = new JObject();
|
||||
}
|
||||
|
||||
var response = await PostAsync($"{_apiUrl}/v1/account/vehicles/{vin}/commands/{command}", new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(reqObj), Encoding.UTF8, "application/json"));
|
||||
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
return null;
|
||||
}
|
||||
|
||||
var commandResult = await response.Content.ReadAsAsync<CommandResponseRoot>();
|
||||
|
||||
return commandResult.commandResponse;
|
||||
}
|
||||
|
||||
|
||||
public async Task<Commandresponse> WaitForCommandCompletion(string statusUrl)
|
||||
{
|
||||
int nullResponseCount = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
await Task.Delay(5000);
|
||||
var result = await PollCommandStatus(statusUrl);
|
||||
if (result == null)
|
||||
{
|
||||
nullResponseCount++;
|
||||
if (nullResponseCount > 5) return null;
|
||||
}
|
||||
if ("inProgress".Equals(result.status, StringComparison.OrdinalIgnoreCase)) continue;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async Task<Commandresponse> InitiateCommandAndWait(string vin, string pin, string command)
|
||||
{
|
||||
var result = await InitiateCommand(vin, pin, command);
|
||||
var endStatus = await WaitForCommandCompletion(result.url);
|
||||
return endStatus;
|
||||
}
|
||||
|
||||
async Task<bool> InitiateCommandAndWaitForSuccess(string vin, string pin, string command)
|
||||
{
|
||||
var result = await InitiateCommandAndWait(vin, pin, command);
|
||||
if (result == null) return false;
|
||||
if ("success".Equals(result.status, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async Task<Commandresponse> PollCommandStatus(string statusUrl)
|
||||
{
|
||||
var response = await GetAsync($"{statusUrl}?units=METRIC");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var result = await response.Content.ReadAsAsync<CommandResponseRoot>();
|
||||
return result.commandResponse;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task<IEnumerable<Vehicle>> GetVehicles()
|
||||
{
|
||||
//these could be parameterized, but we better stick with what the app does
|
||||
var resp = await GetAsync($"{_apiUrl}/v1/account/vehicles?offset=0&limit=10&includeCommands=true&includeEntitlements=true&includeModules=true");
|
||||
|
||||
if (resp.IsSuccessStatusCode)
|
||||
{
|
||||
var outerResult = await resp.Content.ReadAsAsync<VehiclesResponse>();
|
||||
if (outerResult.vehicles != null && outerResult.vehicles.vehicle != null && outerResult.vehicles.vehicle.Length > 0)
|
||||
{
|
||||
return outerResult.vehicles.vehicle;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async Task<Diagnosticresponse[]> GetDiagnostics(string vin, string pin)
|
||||
{
|
||||
var result = await InitiateCommandAndWait(vin, pin, "diagnostics");
|
||||
if (result == null) return null;
|
||||
if ("success".Equals(result.status, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return result.body.diagnosticResponse;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async Task<bool> LockDoor(string vin, string pin)
|
||||
{
|
||||
return await InitiateCommandAndWaitForSuccess(vin, pin, "lockDoor");
|
||||
}
|
||||
|
||||
public async Task<bool> UnlockDoor(string vin, string pin)
|
||||
{
|
||||
return await InitiateCommandAndWaitForSuccess(vin, pin, "unlockDoor");
|
||||
}
|
||||
|
||||
public async Task<bool> Start(string vin, string pin)
|
||||
{
|
||||
return await InitiateCommandAndWaitForSuccess(vin, pin, "start");
|
||||
}
|
||||
|
||||
public async Task<bool> CancelStart(string vin, string pin)
|
||||
{
|
||||
return await InitiateCommandAndWaitForSuccess(vin, pin, "cancelStart");
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> Alert(string vin, string pin)
|
||||
{
|
||||
return await InitiateCommandAndWaitForSuccess(vin, pin, "alert");
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> CancelAlert(string vin, string pin)
|
||||
{
|
||||
return await InitiateCommandAndWaitForSuccess(vin, pin, "cancelAlert");
|
||||
}
|
||||
|
||||
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
522
GM.Api/GMClientBase.cs
Normal file
522
GM.Api/GMClientBase.cs
Normal file
@@ -0,0 +1,522 @@
|
||||
using GM.Api.Models;
|
||||
using GM.Api.Tokens;
|
||||
using JWT;
|
||||
using JWT.Algorithms;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GM.Api
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class API client for GM web services
|
||||
/// </summary>
|
||||
public abstract class GMClientBase
|
||||
{
|
||||
public static int RetryCount { get; set; } = 3;
|
||||
|
||||
//TODO: consistent exception throwing
|
||||
|
||||
string _clientId;
|
||||
string _deviceId;
|
||||
JwtTool _jwtTool;
|
||||
string _apiUrl;
|
||||
string _host;
|
||||
|
||||
HttpClient _client;
|
||||
|
||||
public bool IsUpgraded { get; private set; } = false;
|
||||
bool _isConnected = false;
|
||||
|
||||
/// <summary>
|
||||
/// Contents of the received login token
|
||||
/// May be populated from a cached token
|
||||
/// Refreshed automatically
|
||||
/// </summary>
|
||||
public LoginData LoginData { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Active vehicle configuration
|
||||
/// Must be populated to initiate commands against a car
|
||||
/// </summary>
|
||||
public Vehicle ActiveVehicle { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Callback called when LoginData is updated
|
||||
/// Intended to facilitate updating the stored token
|
||||
/// </summary>
|
||||
public Func<LoginData, Task> TokenUpdateCallback { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Create a new GMClient
|
||||
/// </summary>
|
||||
/// <param name="clientId">Client ID for authentication</param>
|
||||
/// <param name="deviceId">Device ID (should be in the format of a GUID)</param>
|
||||
/// <param name="clientSecret">Client Secret for authentication</param>
|
||||
/// <param name="apiUrl">Base url for the API. Usually https://api.gm.com/api </param>
|
||||
public GMClientBase(string clientId, string deviceId, string clientSecret, string apiUrl)
|
||||
{
|
||||
Setup(clientId, deviceId, clientSecret, apiUrl);
|
||||
}
|
||||
|
||||
void Setup(string clientId, string deviceId, string clientSecret, string apiUrl)
|
||||
{
|
||||
_clientId = clientId;
|
||||
_deviceId = deviceId;
|
||||
_jwtTool = new JwtTool(clientSecret);
|
||||
_apiUrl = apiUrl;
|
||||
var uri = new Uri(_apiUrl);
|
||||
_host = uri.Host;
|
||||
_client = CreateClient(_host);
|
||||
}
|
||||
|
||||
|
||||
static HttpClient CreateClient(string host)
|
||||
{
|
||||
var client = new HttpClient(new HttpClientHandler() { AllowAutoRedirect = true, AutomaticDecompression = System.Net.DecompressionMethods.GZip });
|
||||
|
||||
client.DefaultRequestHeaders.AcceptEncoding.SetValue("gzip");
|
||||
client.DefaultRequestHeaders.Accept.SetValue("application/json");
|
||||
client.DefaultRequestHeaders.AcceptLanguage.SetValue("en-US");
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd("okhttp/3.9.0");
|
||||
client.DefaultRequestHeaders.Host = host;
|
||||
client.DefaultRequestHeaders.MaxForwards = 10;
|
||||
client.DefaultRequestHeaders.ExpectContinue = false;
|
||||
return client;
|
||||
}
|
||||
|
||||
|
||||
#region Client Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Helper wrapper for SendAsync that handles token updates and retries
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="noAuth"></param>
|
||||
/// <returns></returns>
|
||||
async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool noAuth = false)
|
||||
{
|
||||
if (!noAuth)
|
||||
{
|
||||
if (LoginData == null)
|
||||
{
|
||||
throw new InvalidOperationException("Not Logged in");
|
||||
}
|
||||
if (LoginData.IsExpired)
|
||||
{
|
||||
var result = await RefreshToken();
|
||||
if (!result)
|
||||
{
|
||||
throw new InvalidOperationException("Token refresh failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
request.Headers.Authorization = null;
|
||||
}
|
||||
|
||||
int attempt = 0;
|
||||
while (attempt < RetryCount)
|
||||
{
|
||||
attempt++;
|
||||
HttpResponseMessage resp = null;
|
||||
try
|
||||
{
|
||||
resp = await _client.SendAsync(request);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//todo: only catch transient errors
|
||||
//todo: log this
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized || resp.StatusCode == System.Net.HttpStatusCode.Forbidden)
|
||||
{
|
||||
var result = await RefreshToken();
|
||||
if (!result)
|
||||
{
|
||||
throw new InvalidOperationException("Token refresh failed");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
else if (resp.StatusCode == System.Net.HttpStatusCode.BadGateway || resp.StatusCode == System.Net.HttpStatusCode.Conflict || resp.StatusCode == System.Net.HttpStatusCode.GatewayTimeout || resp.StatusCode == System.Net.HttpStatusCode.InternalServerError || resp.StatusCode == System.Net.HttpStatusCode.RequestTimeout || resp.StatusCode == System.Net.HttpStatusCode.ResetContent || resp.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)
|
||||
{
|
||||
//possible transient errors
|
||||
//todo: log this
|
||||
await Task.Delay(500);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
var respMessage = (await resp.Content.ReadAsStringAsync()) ?? "";
|
||||
throw new InvalidOperationException("Request error. StatusCode: " + resp.StatusCode.ToString() + ", msg: " + respMessage);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
//todo: include more info
|
||||
throw new InvalidOperationException("Request failed too many times");
|
||||
}
|
||||
|
||||
|
||||
async Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content, bool noAuth = false)
|
||||
{
|
||||
return await SendAsync(new HttpRequestMessage(HttpMethod.Post, requestUri) { Content = content }, noAuth);
|
||||
}
|
||||
|
||||
async Task<HttpResponseMessage> GetAsync(string requestUri, bool noAuth = false)
|
||||
{
|
||||
return await SendAsync(new HttpRequestMessage(HttpMethod.Get, requestUri), noAuth);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Connect to vehicle. Must be called before issuing commands
|
||||
/// </summary>
|
||||
/// <param name="vin"></param>
|
||||
/// <returns></returns>
|
||||
async Task<Commandresponse> VehicleConnect()
|
||||
{
|
||||
if (ActiveVehicle == null) throw new InvalidOperationException("ActiveVehicle must be populated");
|
||||
using (var response = await PostAsync(ActiveVehicle.GetCommand("connect").Url, new StringContent("{}", Encoding.UTF8, "application/json")))
|
||||
{
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var respString = await response.Content.ReadAsStringAsync();
|
||||
var respObj = JsonConvert.DeserializeObject<CommandResponseRoot>(respString);
|
||||
if (respObj == null || respObj.commandResponse == null) return null;
|
||||
return respObj.commandResponse;
|
||||
}
|
||||
else
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upgrade the token using OnStar PIN
|
||||
/// Allows the execution of privileged commands on the vehicle
|
||||
/// </summary>
|
||||
/// <param name="onStarPin">OnStar PIN</param>
|
||||
/// <returns>Success or not</returns>
|
||||
public async Task<bool> UpgradeLogin(string onStarPin)
|
||||
{
|
||||
var payload = new LoginRequest()
|
||||
{
|
||||
ClientId = _clientId,
|
||||
DeviceId = _deviceId,
|
||||
Credential = onStarPin,
|
||||
CredentialType = "PIN",
|
||||
Nonce = helpers.GenerateNonce(),
|
||||
Timestamp = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK")
|
||||
};
|
||||
|
||||
var token = _jwtTool.EncodeToken(payload);
|
||||
|
||||
using (var response = await PostAsync($"{_apiUrl}/v1/oauth/token/upgrade", new StringContent(token, Encoding.UTF8, "text/plain")))
|
||||
{
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
IsUpgraded = true;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Login to the API via Username and Password
|
||||
/// These credentials are not stored; only exchanged for a token
|
||||
/// The token is maintained by the client
|
||||
/// </summary>
|
||||
/// <param name="username">GM account username</param>
|
||||
/// <param name="password">GM Account password</param>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> Login(string username, string password)
|
||||
{
|
||||
var payload = new LoginRequest()
|
||||
{
|
||||
ClientId = _clientId,
|
||||
DeviceId = _deviceId,
|
||||
GrantType = "password",
|
||||
Nonce = helpers.GenerateNonce(),
|
||||
Password = password,
|
||||
Scope = "onstar gmoc commerce user_trailer msso",
|
||||
Timestamp = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK"),
|
||||
Username = username
|
||||
};
|
||||
|
||||
var token = _jwtTool.EncodeToken(payload);
|
||||
|
||||
using (var response = await PostAsync($"{_apiUrl}/v1/oauth/token", new StringContent(token, Encoding.UTF8, "text/plain"), true))
|
||||
{
|
||||
string rawResponseToken = null;
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
rawResponseToken = await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(rawResponseToken))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var loginTokenData = _jwtTool.DecodeTokenToObject<LoginData>(rawResponseToken);
|
||||
|
||||
LoginData = loginTokenData;
|
||||
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", LoginData.AccessToken);
|
||||
|
||||
//todo: should this be a copy rather than a reference?
|
||||
await TokenUpdateCallback?.Invoke(LoginData);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manually refresh access token
|
||||
/// </summary>
|
||||
/// <returns>Success tru or false</returns>
|
||||
public async Task<bool> RefreshToken()
|
||||
{
|
||||
if (LoginData == null) return false;
|
||||
|
||||
var payload = new LoginRequest()
|
||||
{
|
||||
ClientId = _clientId,
|
||||
DeviceId = _deviceId,
|
||||
GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
||||
Nonce = helpers.GenerateNonce(),
|
||||
Scope = "onstar gmoc commerce user_trailer",
|
||||
Timestamp = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK"),
|
||||
Assertion = LoginData.IdToken
|
||||
};
|
||||
|
||||
var token = _jwtTool.EncodeToken(payload);
|
||||
|
||||
using (var response = await PostAsync($"{_apiUrl}/v1/oauth/token", new StringContent(token, Encoding.UTF8, "text/plain"), true))
|
||||
{
|
||||
|
||||
string rawResponseToken = null;
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
rawResponseToken = await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(rawResponseToken))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/*{
|
||||
"access_token": ,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 1800,
|
||||
"scope": "user_trailer onstar commerce gmoc role_owner",
|
||||
"user_info": {
|
||||
"RemoteUserId": "",
|
||||
"country": ""
|
||||
}
|
||||
}*/
|
||||
// Not sure if the scope needs to be updated, as msso has been removed in the refresh request
|
||||
|
||||
var refreshData = _jwtTool.DecodeTokenToObject<LoginData>(rawResponseToken);
|
||||
|
||||
LoginData.AccessToken = refreshData.AccessToken;
|
||||
LoginData.IssuedAtUtc = refreshData.IssuedAtUtc;
|
||||
LoginData.ExpiresIn = refreshData.ExpiresIn;
|
||||
|
||||
//should we assume the upgrade status is broken?
|
||||
|
||||
|
||||
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", LoginData.AccessToken);
|
||||
|
||||
//todo: should this be a copy rather than a reference?
|
||||
await TokenUpdateCallback?.Invoke(LoginData);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#region Commands
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Submit the initial call for a command
|
||||
/// NOTE: this will be changing to use the URLs defined in vehicle configuration
|
||||
/// </summary>
|
||||
/// <param name="vin">Vehicle VIN</param>
|
||||
/// <param name="pin">OnStar PIN</param>
|
||||
/// <param name="command">command name</param>
|
||||
/// <returns></returns>
|
||||
async Task<Commandresponse> InitiateCommand(string command, JObject requestParameters)
|
||||
{
|
||||
if (ActiveVehicle == null) throw new InvalidOperationException("ActiveVehicle must be populated");
|
||||
|
||||
var cmdInfo = ActiveVehicle.GetCommand(command);
|
||||
|
||||
if (cmdInfo == null) throw new InvalidOperationException("Unsupported command");
|
||||
|
||||
if (cmdInfo.IsPrivSessionRequired.GetValueOrDefault())
|
||||
{
|
||||
if (!IsUpgraded)
|
||||
{
|
||||
//TODO: need to determine how long an upgrade lasts - do we reset it when refreshing the token?
|
||||
// Also if the android app saves the PIN, should we save the PIN?
|
||||
throw new InvalidOperationException("Command requires upgraded login");
|
||||
}
|
||||
}
|
||||
|
||||
if (!_isConnected)
|
||||
{
|
||||
await VehicleConnect();
|
||||
_isConnected = true;
|
||||
}
|
||||
|
||||
|
||||
JObject reqObj = new JObject();
|
||||
|
||||
if (requestParameters != null)
|
||||
{
|
||||
reqObj[$"{command}Request"] = requestParameters;
|
||||
}
|
||||
|
||||
|
||||
|
||||
using (var response = await PostAsync(cmdInfo.Url, new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(reqObj), Encoding.UTF8, "application/json")))
|
||||
{
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
//todo: is this needed with the fancy post?
|
||||
return null;
|
||||
}
|
||||
|
||||
var commandResult = await response.Content.ReadAsAsync<CommandResponseRoot>();
|
||||
|
||||
return commandResult.commandResponse;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Periodically poll the status of a command, only returning after it succeeds or fails
|
||||
/// </summary>
|
||||
/// <param name="statusUrl">statusUrl returned when the command was initiated</param>
|
||||
/// <returns>Response from final poll</returns>
|
||||
async Task<Commandresponse> WaitForCommandCompletion(string statusUrl)
|
||||
{
|
||||
int nullResponseCount = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
await Task.Delay(5000);
|
||||
var result = await PollCommandStatus(statusUrl);
|
||||
if (result == null)
|
||||
{
|
||||
nullResponseCount++;
|
||||
if (nullResponseCount > 5) return null;
|
||||
}
|
||||
if ("inProgress".Equals(result.status, StringComparison.OrdinalIgnoreCase)) continue;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected async Task<Commandresponse> InitiateCommandAndWait(string command, JObject requestParameters)
|
||||
{
|
||||
var result = await InitiateCommand(command, requestParameters);
|
||||
var endStatus = await WaitForCommandCompletion(result.url);
|
||||
return endStatus;
|
||||
}
|
||||
|
||||
protected async Task<bool> InitiateCommandAndWaitForSuccess(string command, JObject requestParameters)
|
||||
{
|
||||
var result = await InitiateCommandAndWait(command, requestParameters);
|
||||
if (result == null) return false;
|
||||
if ("success".Equals(result.status, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async Task<Commandresponse> PollCommandStatus(string statusUrl)
|
||||
{
|
||||
var response = await GetAsync($"{statusUrl}?units=METRIC");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var result = await response.Content.ReadAsAsync<CommandResponseRoot>();
|
||||
return result.commandResponse;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task<IEnumerable<Vehicle>> GetVehicles()
|
||||
{
|
||||
//these could be parameterized, but we better stick with what the app does
|
||||
var resp = await GetAsync($"{_apiUrl}/v1/account/vehicles?offset=0&limit=10&includeCommands=true&includeEntitlements=true&includeModules=true");
|
||||
|
||||
if (resp.IsSuccessStatusCode)
|
||||
{
|
||||
var outerResult = await resp.Content.ReadAsAsync<VehiclesResponse>();
|
||||
if (outerResult.Vehicles != null && outerResult.Vehicles.Vehicle != null && outerResult.Vehicles.Vehicle.Length > 0)
|
||||
{
|
||||
return outerResult.Vehicles.Vehicle;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
106
GM.Api/GenericGMClient.cs
Normal file
106
GM.Api/GenericGMClient.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using GM.Api.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GM.Api
|
||||
{
|
||||
/// <summary>
|
||||
/// Generic implementation of GM Client supporting a limited set of commands or manually defined commands
|
||||
/// </summary>
|
||||
public class GenericGMClient : GMClientBase
|
||||
{
|
||||
public GenericGMClient(string clientId, string deviceId, string clientSecret, string apiUrl) : base(clientId, deviceId, clientSecret, apiUrl)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public async Task<Diagnosticresponse[]> GetDiagnostics()
|
||||
{
|
||||
var cmdInfo = ActiveVehicle.GetCommand("diagnostics");
|
||||
|
||||
var reqObj = new JObject()
|
||||
{
|
||||
["diagnosticItem"] = new JArray(cmdInfo.CommandData.SupportedDiagnostics.SupportedDiagnostic)
|
||||
};
|
||||
|
||||
var result = await InitiateCommandAndWait("diagnostics", reqObj);
|
||||
if (result == null) return null;
|
||||
if ("success".Equals(result.status, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return result.body.diagnosticResponse;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async Task<Commandresponse> IssueCommand(string commandName, JObject parameters = null)
|
||||
{
|
||||
return await InitiateCommandAndWait(commandName, parameters);
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> LockDoor(string pin)
|
||||
{
|
||||
var reqObj = new JObject()
|
||||
{
|
||||
["delay"] = 0
|
||||
};
|
||||
|
||||
|
||||
return await InitiateCommandAndWaitForSuccess("lockDoor", reqObj);
|
||||
}
|
||||
|
||||
public async Task<bool> UnlockDoor(string pin)
|
||||
{
|
||||
|
||||
var reqObj = new JObject()
|
||||
{
|
||||
["delay"] = 0
|
||||
};
|
||||
|
||||
return await InitiateCommandAndWaitForSuccess("unlockDoor", reqObj);
|
||||
}
|
||||
|
||||
public async Task<bool> Start(string pin)
|
||||
{
|
||||
return await InitiateCommandAndWaitForSuccess("start", null);
|
||||
}
|
||||
|
||||
public async Task<bool> CancelStart(string pin)
|
||||
{
|
||||
return await InitiateCommandAndWaitForSuccess("cancelStart", null);
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> Alert(string pin)
|
||||
{
|
||||
var reqObj = new JObject()
|
||||
{
|
||||
["action"] = new JArray() { "Honk", "Flash" },
|
||||
["delay"] = 0,
|
||||
["duration"] = 1,
|
||||
["override"] = new JArray() { "DoorOpen", "IgnitionOn" }
|
||||
};
|
||||
|
||||
|
||||
return await InitiateCommandAndWaitForSuccess("alert", reqObj);
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> CancelAlert(string pin)
|
||||
{
|
||||
return await InitiateCommandAndWaitForSuccess("cancelAlert", null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -5,144 +5,6 @@ using System.Linq;
|
||||
|
||||
namespace GM.Api.Models
|
||||
{
|
||||
|
||||
|
||||
|
||||
//public class DiagnosticReader
|
||||
//{
|
||||
|
||||
// IEnumerable<Diagnosticresponse> _dr;
|
||||
|
||||
// public DiagnosticReader(IEnumerable<Diagnosticresponse> elements)
|
||||
// {
|
||||
// _dr = elements;
|
||||
// }
|
||||
|
||||
// public float AmbientAirTempCelcius => float.Parse((from f in _dr
|
||||
// where f.name == "AMBIENT AIR TEMPERATURE"
|
||||
// from r in f.diagnosticElement
|
||||
// where r.name == "AMBIENT AIR TEMPERATURE"
|
||||
// select r.value).FirstOrDefault());
|
||||
// public string ChargerPowerLevel => (from f in _dr
|
||||
// where f.name == "CHARGER POWER LEVEL"
|
||||
// from r in f.diagnosticElement
|
||||
// where r.name == "CHARGER POWER LEVEL"
|
||||
// select r.value).FirstOrDefault();
|
||||
|
||||
// public float EvBatteryLevelPercent => float.Parse((from f in _dr
|
||||
// where f.name == "EV BATTERY LEVEL"
|
||||
// from r in f.diagnosticElement
|
||||
// where r.name == "EV BATTERY LEVEL"
|
||||
// select r.value).FirstOrDefault());
|
||||
|
||||
|
||||
//}
|
||||
|
||||
|
||||
//public static class DiagnosticHelper
|
||||
//{
|
||||
// public static float GetElectricEconomyKwh(this IEnumerable<Diagnosticresponse> elements)
|
||||
// {
|
||||
// var itm = float.Parse((from f in elements
|
||||
// where f.name == "ENERGY EFFICIENCY"
|
||||
// from r in f.diagnosticElement
|
||||
// where r.name == "ELECTRIC ECONOMY"
|
||||
// select r.value).FirstOrDefault());
|
||||
|
||||
// return itm;
|
||||
// }
|
||||
|
||||
// public static float GetLifetimeEfficiencyKwh(this IEnumerable<Diagnosticresponse> elements)
|
||||
// {
|
||||
// var itm = float.Parse((from f in elements
|
||||
// where f.name == "ENERGY EFFICIENCY"
|
||||
// from r in f.diagnosticElement
|
||||
// where r.name == "LIFETIME EFFICIENCY"
|
||||
// select r.value).FirstOrDefault());
|
||||
|
||||
// return itm;
|
||||
// }
|
||||
|
||||
// public static float GetLifetimeMpgE(this IEnumerable<Diagnosticresponse> elements)
|
||||
// {
|
||||
// var itm = float.Parse((from f in elements
|
||||
// where f.name == "ENERGY EFFICIENCY"
|
||||
// from r in f.diagnosticElement
|
||||
// where r.name == "LIFETIME MPGE"
|
||||
// select r.value).FirstOrDefault());
|
||||
|
||||
// return itm;
|
||||
// }
|
||||
|
||||
// public static float GetOdometerKm(this IEnumerable<Diagnosticresponse> elements)
|
||||
// {
|
||||
// var itm = float.Parse((from f in elements
|
||||
// where f.name == "ENERGY EFFICIENCY"
|
||||
// from r in f.diagnosticElement
|
||||
// where r.name == "ODOMETER"
|
||||
// select r.value).FirstOrDefault());
|
||||
|
||||
// return itm;
|
||||
// }
|
||||
|
||||
// public static float GetEvBatteryLevelPercent(this IEnumerable<Diagnosticresponse> elements)
|
||||
// {
|
||||
// var itm = float.Parse((from f in elements
|
||||
// where f.name == "EV BATTERY LEVEL"
|
||||
// from r in f.diagnosticElement
|
||||
// where r.name == "EV BATTERY LEVEL"
|
||||
// select r.value).FirstOrDefault());
|
||||
|
||||
// return itm;
|
||||
// }
|
||||
|
||||
//}
|
||||
|
||||
|
||||
public class DiagnosticRequestRoot
|
||||
{
|
||||
public static readonly string[] DefaultItems = new string[]
|
||||
{
|
||||
"ENGINE COOLANT TEMP",
|
||||
"ENGINE RPM",
|
||||
"HV BATTERY ESTIMATED CAPACITY",
|
||||
"LAST TRIP FUEL ECONOMY",
|
||||
"ENERGY EFFICIENCY",
|
||||
"HYBRID BATTERY MINIMUM TEMPERATURE",
|
||||
"EV ESTIMATED CHARGE END",
|
||||
"EV BATTERY LEVEL",
|
||||
"EV PLUG VOLTAGE",
|
||||
"ODOMETER",
|
||||
"CHARGER POWER LEVEL",
|
||||
"LIFETIME EV ODOMETER",
|
||||
"EV PLUG STATE",
|
||||
"EV CHARGE STATE",
|
||||
"TIRE PRESSURE",
|
||||
"AMBIENT AIR TEMPERATURE",
|
||||
"LAST TRIP DISTANCE",
|
||||
"INTERM VOLT BATT VOLT",
|
||||
"GET COMMUTE SCHEDULE",
|
||||
"GET CHARGE MODE",
|
||||
"EV SCHEDULED CHARGE START",
|
||||
"VEHICLE RANGE"
|
||||
};
|
||||
|
||||
|
||||
//public Diagnosticsrequest diagnosticsRequest { get; set; }
|
||||
}
|
||||
|
||||
//public class Diagnosticsrequest
|
||||
//{
|
||||
// public string[] diagnosticItem { get; set; }
|
||||
//}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public class ResponseBody
|
||||
{
|
||||
public Diagnosticresponse[] diagnosticResponse { get; set; }
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace GM.Api.Models
|
||||
@@ -8,44 +10,90 @@ namespace GM.Api.Models
|
||||
|
||||
public class VehiclesResponse
|
||||
{
|
||||
public Vehicles vehicles { get; set; }
|
||||
[JsonProperty("vehicles")]
|
||||
public Vehicles Vehicles { get; set; }
|
||||
}
|
||||
|
||||
public class Vehicles
|
||||
{
|
||||
public string size { get; set; }
|
||||
public Vehicle[] vehicle { get; set; }
|
||||
[JsonProperty("size")]
|
||||
public string Size { get; set; }
|
||||
|
||||
[JsonProperty("vehicle")]
|
||||
public Vehicle[] Vehicle { get; set; }
|
||||
}
|
||||
|
||||
public class Vehicle
|
||||
{
|
||||
public string vin { get; set; }
|
||||
public string make { get; set; }
|
||||
public string model { get; set; }
|
||||
public string year { get; set; }
|
||||
public string manufacturer { get; set; }
|
||||
public string bodyStyle { get; set; }
|
||||
public string phone { get; set; }
|
||||
public string unitType { get; set; }
|
||||
public string onstarStatus { get; set; }
|
||||
public string url { get; set; }
|
||||
public string isInPreActivation { get; set; }
|
||||
public Insuranceinfo insuranceInfo { get; set; }
|
||||
public string enrolledInContinuousCoverage { get; set; }
|
||||
public Commands commands { get; set; }
|
||||
public Modules modules { get; set; }
|
||||
public Entitlements entitlements { get; set; }
|
||||
public string propulsionType { get; set; }
|
||||
[JsonProperty("vin")]
|
||||
public string Vin { get; set; }
|
||||
|
||||
[JsonProperty("make")]
|
||||
public string Make { get; set; }
|
||||
|
||||
[JsonProperty("model")]
|
||||
public string Model { get; set; }
|
||||
|
||||
[JsonProperty("year")]
|
||||
public string Year { get; set; }
|
||||
|
||||
[JsonProperty("manufacturer")]
|
||||
public string Manufacturer { get; set; }
|
||||
|
||||
[JsonProperty("bodyStyle")]
|
||||
public string BodyStyle { get; set; }
|
||||
|
||||
[JsonProperty("phone")]
|
||||
public string Phone { get; set; }
|
||||
|
||||
[JsonProperty("unitType")]
|
||||
public string UnitType { get; set; }
|
||||
|
||||
[JsonProperty("onstarStatus")]
|
||||
public string OnStarStatus { get; set; }
|
||||
|
||||
[JsonProperty("url")]
|
||||
public string Url { get; set; }
|
||||
|
||||
[JsonProperty("isInPreActivation")]
|
||||
public bool? IsInPreActivation { get; set; }
|
||||
|
||||
[JsonProperty("insuranceInfo")]
|
||||
public InsuranceInfo InsuranceInfo { get; set; }
|
||||
|
||||
[JsonProperty("enrolledInContinuousCoverage")]
|
||||
public bool? EnrolledInContinuousCoverage { get; set; }
|
||||
|
||||
[JsonProperty("commands")]
|
||||
public Commands Commands { get; set; }
|
||||
|
||||
[JsonProperty("modules")]
|
||||
public Modules Modules { get; set; }
|
||||
|
||||
[JsonProperty("entitlements")]
|
||||
public Entitlements Entitlements { get; set; }
|
||||
|
||||
[JsonProperty("propulsionType")]
|
||||
public string PropulsionType { get; set; }
|
||||
|
||||
|
||||
public Command GetCommand(string name)
|
||||
{
|
||||
return (from f in Commands.Command where f.Name.Equals(name, StringComparison.Ordinal) select f).FirstOrDefault();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class Insuranceinfo
|
||||
public class InsuranceInfo
|
||||
{
|
||||
public InsuranceAgent insuranceAgent { get; set; }
|
||||
[JsonProperty("insuranceAgent")]
|
||||
public InsuranceAgent InsuranceAgent { get; set; }
|
||||
}
|
||||
|
||||
public class InsuranceAgent
|
||||
{
|
||||
public Phone phone { get; set; }
|
||||
[JsonProperty("phone")]
|
||||
public Phone Phone { get; set; }
|
||||
}
|
||||
|
||||
public class Phone
|
||||
@@ -54,50 +102,74 @@ namespace GM.Api.Models
|
||||
|
||||
public class Commands
|
||||
{
|
||||
public Command[] command { get; set; }
|
||||
[JsonProperty("command")]
|
||||
public Command[] Command { get; set; }
|
||||
}
|
||||
|
||||
public class Command
|
||||
{
|
||||
public string name { get; set; }
|
||||
public string description { get; set; }
|
||||
public string url { get; set; }
|
||||
public string isPrivSessionRequired { get; set; }
|
||||
public CommandData commandData { get; set; }
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty("description")]
|
||||
public string Description { get; set; }
|
||||
|
||||
[JsonProperty("url")]
|
||||
public string Url { get; set; }
|
||||
|
||||
[JsonProperty("isPrivSessionRequired")]
|
||||
public bool? IsPrivSessionRequired { get; set; }
|
||||
|
||||
[JsonProperty("commandData")]
|
||||
public CommandData CommandData { get; set; }
|
||||
}
|
||||
|
||||
public class CommandData
|
||||
{
|
||||
public SupportedDiagnostics supportedDiagnostics { get; set; }
|
||||
[JsonProperty("supportedDiagnostics")]
|
||||
public SupportedDiagnostics SupportedDiagnostics { get; set; }
|
||||
}
|
||||
|
||||
public class SupportedDiagnostics
|
||||
{
|
||||
public string[] supportedDiagnostic { get; set; }
|
||||
[JsonProperty("supportedDiagnostic")]
|
||||
public string[] SupportedDiagnostic { get; set; }
|
||||
}
|
||||
|
||||
public class Modules
|
||||
{
|
||||
public Module[] module { get; set; }
|
||||
[JsonProperty("module")]
|
||||
public Module[] Module { get; set; }
|
||||
}
|
||||
|
||||
public class Module
|
||||
{
|
||||
public string moduleType { get; set; }
|
||||
public string moduleCapability { get; set; }
|
||||
[JsonProperty("moduleType")]
|
||||
public string ModuleType { get; set; }
|
||||
|
||||
[JsonProperty("moduleCapability")]
|
||||
public string ModuleCapability { get; set; }
|
||||
}
|
||||
|
||||
public class Entitlements
|
||||
{
|
||||
public Entitlement[] entitlement { get; set; }
|
||||
[JsonProperty("entitlement")]
|
||||
public Entitlement[] Entitlement { get; set; }
|
||||
}
|
||||
|
||||
public class Entitlement
|
||||
{
|
||||
public string id { get; set; }
|
||||
public string eligible { get; set; }
|
||||
public string ineligibleReasonCode { get; set; }
|
||||
public string notificationCapable { get; set; }
|
||||
[JsonProperty("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[JsonProperty("eligible")]
|
||||
public bool? Eligible { get; set; }
|
||||
|
||||
[JsonProperty("ineligibleReasonCode")]
|
||||
public string IneligibleReasonCode { get; set; }
|
||||
|
||||
[JsonProperty("notificationCapable")]
|
||||
public string NotificationCapable { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -20,11 +20,11 @@ namespace GM.WindowsUI
|
||||
/// </summary>
|
||||
public partial class LoginWindow : Window
|
||||
{
|
||||
GMClient _client;
|
||||
GMClientBase _client;
|
||||
|
||||
public bool Success { get; private set; } = false;
|
||||
|
||||
public LoginWindow(GMClient client)
|
||||
public LoginWindow(GMClientBase client)
|
||||
{
|
||||
_client = client;
|
||||
InitializeComponent();
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:GM.WindowsUI"
|
||||
mc:Ignorable="d"
|
||||
Title="GM Vehicle Control" Height="481.809" Width="559" Loaded="Window_Loaded">
|
||||
Title="GM Vehicle Control" Height="481.809" Width="662" Loaded="Window_Loaded">
|
||||
<Grid>
|
||||
<PasswordBox Name="txtPin" HorizontalAlignment="Left" Margin="97,140,0,0" VerticalAlignment="Top" Width="144"/>
|
||||
<Label Content="OnStar PIN" HorizontalAlignment="Left" Margin="22,135,0,0" VerticalAlignment="Top"/>
|
||||
<Button Name="btnLogin" Content="Login" HorizontalAlignment="Left" Margin="42,41,0,0" VerticalAlignment="Top" Width="153" Click="BtnLogin_Click" Height="46"/>
|
||||
|
||||
<GroupBox Header="Actions" Name="grpActions" HorizontalAlignment="Left" Height="231" Margin="22,166,0,0" VerticalAlignment="Top" Width="219">
|
||||
<GroupBox Header="Actions" Name="grpActions" HorizontalAlignment="Left" Height="231" Margin="22,166,0,0" VerticalAlignment="Top" Width="323">
|
||||
<Grid>
|
||||
<Button x:Name="btnLock" Content="Lock Doors" HorizontalAlignment="Left" Margin="14,13,0,0" VerticalAlignment="Top" Width="79" Height="49" Click="BtnLock_Click"/>
|
||||
<Button x:Name="btnStart" Content="Remote Start" HorizontalAlignment="Left" Margin="14,78,0,0" VerticalAlignment="Top" Width="79" Height="49" Click="BtnStart_Click"/>
|
||||
@@ -19,11 +19,14 @@
|
||||
<Button x:Name="btnUnlock" Content="Unlock Doors" HorizontalAlignment="Left" Margin="114,13,0,0" VerticalAlignment="Top" Width="79" Height="49" Click="BtnUnlock_Click"/>
|
||||
<Button x:Name="btnStop" Content="Remote Stop" HorizontalAlignment="Left" Margin="114,78,0,0" VerticalAlignment="Top" Width="79" Height="49" Click="BtnStop_Click"/>
|
||||
<Button x:Name="btnCancelAlert" Content="Stop Alarm" HorizontalAlignment="Left" Margin="114,145,0,0" VerticalAlignment="Top" Width="79" Height="49" Click="BtnCancelAlert_Click"/>
|
||||
<Button x:Name="btnDiagnostics" Content="Get Diagnostics" HorizontalAlignment="Left" Margin="205,13,0,0" VerticalAlignment="Top" Width="96" Height="49" Click="BtnDiagnostics_Click"/>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
<Label Name ="lblStatus" Content="Not Logged In" HorizontalAlignment="Left" Margin="10,402,0,0" VerticalAlignment="Top" FontSize="24" Width="281"/>
|
||||
<Label Name ="lblStatus" Content="Not Logged In" HorizontalAlignment="Left" Margin="10,402,0,0" VerticalAlignment="Top" FontSize="24" Width="539"/>
|
||||
<ComboBox x:Name="cmbVehicle" HorizontalAlignment="Left" Margin="97,101,0,0" VerticalAlignment="Top" Width="399" SelectionChanged="CmbVehicle_SelectionChanged"/>
|
||||
<Label Content="Vehicle" HorizontalAlignment="Left" Margin="42,101,0,0" VerticalAlignment="Top"/>
|
||||
<TextBox Name="txtOutput" HorizontalAlignment="Left" Height="239" Margin="362,158,0,0" VerticalAlignment="Top" Width="260" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"/>
|
||||
<Label Content="Diag Output" HorizontalAlignment="Left" Margin="362,132,0,0" VerticalAlignment="Top"/>
|
||||
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace GM.WindowsUI
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
GMClient _client;
|
||||
GenericGMClient _client;
|
||||
|
||||
|
||||
GmConfiguration _globalConfig;
|
||||
@@ -39,7 +39,7 @@ namespace GM.WindowsUI
|
||||
|
||||
Vehicle[] _vehicles = null;
|
||||
|
||||
Vehicle _selectedVehicle;
|
||||
//Vehicle _selectedVehicle;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
@@ -60,7 +60,7 @@ namespace GM.WindowsUI
|
||||
}
|
||||
|
||||
//todo: maybe the client reads the config and takes the brand and device id as param?
|
||||
_client = new GMClient(_clientCredentials.client_id, Properties.Settings.Default.DeviceId, _clientCredentials.client_secret, _apiConfig.url);
|
||||
_client = new GenericGMClient(_clientCredentials.client_id, Properties.Settings.Default.DeviceId, _clientCredentials.client_secret, _apiConfig.url);
|
||||
_client.TokenUpdateCallback = TokenUpdateHandler;
|
||||
|
||||
if (!string.IsNullOrEmpty(Properties.Settings.Default.LoginData))
|
||||
@@ -173,7 +173,7 @@ namespace GM.WindowsUI
|
||||
|
||||
foreach (var vehicle in _vehicles)
|
||||
{
|
||||
cmbVehicle.Items.Add($"{vehicle.year} {vehicle.model} ({vehicle.vin})");
|
||||
cmbVehicle.Items.Add($"{vehicle.Year} {vehicle.Model} ({vehicle.Vin})");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(Properties.Settings.Default.Vin))
|
||||
@@ -181,7 +181,7 @@ namespace GM.WindowsUI
|
||||
bool found = false;
|
||||
for (int i = 0; i < _vehicles.Length; i++)
|
||||
{
|
||||
if (_vehicles[i].vin.Equals(Properties.Settings.Default.Vin, StringComparison.OrdinalIgnoreCase))
|
||||
if (_vehicles[i].Vin.Equals(Properties.Settings.Default.Vin, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
found = true;
|
||||
cmbVehicle.SelectedIndex = i;
|
||||
@@ -218,12 +218,35 @@ namespace GM.WindowsUI
|
||||
btnLogin.IsEnabled = false;
|
||||
}
|
||||
|
||||
|
||||
async Task<bool> HandleUpgrade()
|
||||
{
|
||||
if (!_client.IsUpgraded)
|
||||
{
|
||||
if (string.IsNullOrEmpty(txtPin.Password))
|
||||
{
|
||||
MessageBox.Show("OnStar PIN required");
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = await _client.UpgradeLogin(txtPin.Password);
|
||||
if (!result)
|
||||
{
|
||||
MessageBox.Show("Login upgrade failed!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private async void BtnLock_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!await HandleUpgrade()) return;
|
||||
|
||||
grpActions.IsEnabled = false;
|
||||
btnLogin.IsEnabled = false;
|
||||
lblStatus.Content = "Locking (Please wait)";
|
||||
var success = await _client.LockDoor(_selectedVehicle.vin, txtPin.Password);
|
||||
var success = await _client.LockDoor(txtPin.Password);
|
||||
if (success)
|
||||
{
|
||||
lblStatus.Content = "Locked Successfully";
|
||||
@@ -239,10 +262,11 @@ namespace GM.WindowsUI
|
||||
|
||||
private async void BtnUnlock_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!await HandleUpgrade()) return;
|
||||
grpActions.IsEnabled = false;
|
||||
btnLogin.IsEnabled = false;
|
||||
lblStatus.Content = "Unlocking (Please wait)";
|
||||
var success = await _client.UnlockDoor(_selectedVehicle.vin, txtPin.Password);
|
||||
var success = await _client.UnlockDoor(txtPin.Password);
|
||||
if (success)
|
||||
{
|
||||
lblStatus.Content = "Unlocked Successfully";
|
||||
@@ -257,10 +281,11 @@ namespace GM.WindowsUI
|
||||
|
||||
private async void BtnStart_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!await HandleUpgrade()) return;
|
||||
grpActions.IsEnabled = false;
|
||||
btnLogin.IsEnabled = false;
|
||||
lblStatus.Content = "Starting (Please wait)";
|
||||
var success = await _client.Start(_selectedVehicle.vin, txtPin.Password);
|
||||
var success = await _client.Start(txtPin.Password);
|
||||
if (success)
|
||||
{
|
||||
lblStatus.Content = "Started Successfully";
|
||||
@@ -275,10 +300,11 @@ namespace GM.WindowsUI
|
||||
|
||||
private async void BtnStop_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!await HandleUpgrade()) return;
|
||||
grpActions.IsEnabled = false;
|
||||
btnLogin.IsEnabled = false;
|
||||
lblStatus.Content = "Stopping (Please wait)";
|
||||
var success = await _client.CancelStart(_selectedVehicle.vin, txtPin.Password);
|
||||
var success = await _client.CancelStart(txtPin.Password);
|
||||
if (success)
|
||||
{
|
||||
lblStatus.Content = "Stopped Successfully";
|
||||
@@ -293,10 +319,11 @@ namespace GM.WindowsUI
|
||||
|
||||
private async void BtnAlert_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!await HandleUpgrade()) return;
|
||||
grpActions.IsEnabled = false;
|
||||
btnLogin.IsEnabled = false;
|
||||
lblStatus.Content = "Alarming (Please wait)";
|
||||
var success = await _client.Alert(_selectedVehicle.vin, txtPin.Password);
|
||||
var success = await _client.Alert(txtPin.Password);
|
||||
if (success)
|
||||
{
|
||||
lblStatus.Content = "Alarmed Successfully";
|
||||
@@ -311,10 +338,11 @@ namespace GM.WindowsUI
|
||||
|
||||
private async void BtnCancelAlert_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!await HandleUpgrade()) return;
|
||||
grpActions.IsEnabled = false;
|
||||
btnLogin.IsEnabled = false;
|
||||
lblStatus.Content = "Stopping Alarm (Please wait)";
|
||||
var success = await _client.CancelAlert(_selectedVehicle.vin, txtPin.Password);
|
||||
var success = await _client.CancelAlert(txtPin.Password);
|
||||
if (success)
|
||||
{
|
||||
lblStatus.Content = "Alarmed Stopped Successfully";
|
||||
@@ -343,18 +371,31 @@ namespace GM.WindowsUI
|
||||
{
|
||||
if (_vehicles == null || _vehicles.Length == 0 || cmbVehicle.SelectedIndex < 0)
|
||||
{
|
||||
_selectedVehicle = null;
|
||||
_client.ActiveVehicle = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedVehicle = _vehicles[cmbVehicle.SelectedIndex];
|
||||
_client.ActiveVehicle = _vehicles[cmbVehicle.SelectedIndex];
|
||||
|
||||
Properties.Settings.Default.Vin = _selectedVehicle.vin;
|
||||
Properties.Settings.Default.Vin = _client.ActiveVehicle.Vin;
|
||||
Properties.Settings.Default.Save();
|
||||
|
||||
//todo: populate available actions
|
||||
//todo: update client state instead of local variable?
|
||||
|
||||
}
|
||||
|
||||
private async void BtnDiagnostics_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!await HandleUpgrade()) return;
|
||||
grpActions.IsEnabled = false;
|
||||
btnLogin.IsEnabled = false;
|
||||
lblStatus.Content = "Getting Diagnostics (Please Wait)...";
|
||||
var details = await _client.GetDiagnostics();
|
||||
txtOutput.Text = JsonConvert.SerializeObject(details, Formatting.Indented);
|
||||
|
||||
grpActions.IsEnabled = true;
|
||||
btnLogin.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user