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:
Anonymous
2019-08-10 17:32:41 -04:00
parent 4dcebba1e8
commit cd36d6d8d6
8 changed files with 803 additions and 730 deletions

View File

@@ -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
View 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
View 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);
}
}
}

View File

@@ -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; }

View File

@@ -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; }
}

View File

@@ -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();

View File

@@ -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>

View File

@@ -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;
}
}
}