Skip to content

Commit

Permalink
Added support for logging into Gateway
Browse files Browse the repository at this point in the history
  • Loading branch information
DoctorMcKay committed Apr 30, 2021
1 parent 151d87d commit fcd00d3
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 15 deletions.
2 changes: 1 addition & 1 deletion .idea/.idea.HSPI_TeslaPowerwall/.idea/indexLayout.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 0 additions & 7 deletions .idea/.idea.HSPI_TeslaPowerwall/riderModule.iml

This file was deleted.

46 changes: 42 additions & 4 deletions HSPI_TeslaPowerwall/HSPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public class HSPI : HspiBase

private PowerwallClient _client;
private string _gatewayIp = "";
private string _gatewayUsername = "";
private string _gatewayPassword = "";
private GatewayDeviceRefSet _devRefSet;
private Timer _pollTimer;
private IPlugInAPI.enumInterfaceStatus _interfaceStatus = IPlugInAPI.enumInterfaceStatus.OK;
Expand Down Expand Up @@ -59,6 +61,8 @@ private async void CheckGatewayConnection() {
this._pollTimer?.Stop();

this._gatewayIp = hs.GetINISetting("GatewayNetwork", "ip", "", IniFilename);
this._gatewayUsername = hs.GetINISetting("GatewayCredentials", "username", "", IniFilename);
this._gatewayPassword = hs.GetINISetting("GatewayCredentials", "password", "", IniFilename);

Program.WriteLog(LogType.Info, $"Attempting to connect to Gateway at IP \"{this._gatewayIp}\"");

Expand All @@ -69,7 +73,7 @@ private async void CheckGatewayConnection() {
return;
}

this._client = new PowerwallClient(this._gatewayIp);
this._client = new PowerwallClient(this._gatewayIp, this._gatewayUsername, this._gatewayPassword);

try
{
Expand Down Expand Up @@ -130,6 +134,23 @@ private string BuildSettingsPage(string user, int userRights, string queryString
sb.Append(textBox.Build());
sb.Append("</td></tr>");

sb.Append("<tr><td class=\"tablecell\" style=\"width:200px\" align=\"left\">Gateway Customer Email:</td>");
sb.Append("<td class=\"tablecell\">");
textBox = new clsJQuery.jqTextBox("GatewayUsername", "text", this._gatewayUsername, pageName, 30, true);
sb.Append(textBox.Build());
sb.Append("</td></tr>");

sb.Append("<tr><td class=\"tablecell\" style=\"width:200px\" align=\"left\">Gateway Customer Password:</td>");
sb.Append("<td class=\"tablecell\">");
textBox = new clsJQuery.jqTextBox("GatewayPassword", "password", this._gatewayPassword.Length == 0 ? "" : "*****", pageName, 30, true);
sb.Append(textBox.Build());
sb.Append("</td></tr>");

sb.Append("<tr><td class=\"tablecell\" colspan=\"2\" align=\"left\">");
sb.Append("<p>Prior to Gateway firmware version 20.49.0, authentication was not required to retrieve energy statistics. In later versions, authentication is required.</p>");
sb.Append("<p><b>These credentials <u>are not</u> your Tesla.com or Tesla app credentials.</b> These credentials are set in the Gateway's web administration panel, which can be accessed at https://your.gateway.ip on your local network.</p>");
sb.Append("</td></tr>");

sb.Append("</table>");

clsJQuery.jqButton doneBtn = new clsJQuery.jqButton("DoneBtn", "Done", pageName, false);
Expand Down Expand Up @@ -160,9 +181,26 @@ public override string PostBackProc(string page, string data, string user, int u
NameValueCollection postData = HttpUtility.ParseQueryString(data);

string gwIp = postData.Get("GatewayIP");
hs.SaveINISetting("GatewayNetwork", "ip", gwIp, IniFilename);
this._gatewayIp = gwIp;
Program.WriteLog(LogType.Info, $"Updating Gateway IP to \"{gwIp}\"");
if (gwIp != null) {
hs.SaveINISetting("GatewayNetwork", "ip", gwIp, IniFilename);
this._gatewayIp = gwIp;
Program.WriteLog(LogType.Info, $"Updating Gateway IP to \"{gwIp}\"");
}

string gwUsername = postData.Get("GatewayUsername");
if (gwUsername != null) {
hs.SaveINISetting("GatewayCredentials", "username", gwUsername, IniFilename);
this._gatewayUsername = gwUsername;
Program.WriteLog(LogType.Info, $"Updating Gateway username to \"{gwUsername}\"");
}

string gwPassword = postData.Get("GatewayPassword");
if (gwPassword != null && gwPassword != "*****") {
hs.SaveINISetting("GatewayCredentials", "password", gwPassword, IniFilename);
this._gatewayPassword = gwPassword;
Program.WriteLog(LogType.Info, "Updating Gateway password");
}

CheckGatewayConnection();

return "";
Expand Down
95 changes: 92 additions & 3 deletions HSPI_TeslaPowerwall/PowerwallClient.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Script.Serialization;
using Scheduler;
Expand All @@ -8,19 +12,81 @@ namespace HSPI_TeslaPowerwall
{
public class PowerwallClient
{
public DateTime LastLogin { get; private set; }
public bool LoggingIn { get; private set; }

private const int RequestTimeoutMs = 10000;

private readonly string _ipAddress;
private readonly HttpClient _httpClient;
private readonly JavaScriptSerializer _jsonSerializer;
private readonly string _email;
private readonly string _password;

public PowerwallClient(string ipAddress) {
public PowerwallClient(string ipAddress, string email, string password) {
this._ipAddress = ipAddress;
HttpClientHandler handler = new HttpClientHandler();
this._httpClient = new HttpClient(handler);
this._jsonSerializer = new JavaScriptSerializer();
this._email = email;
this._password = password;

// Powerwall Gateway uses a self-signed certificate, so let's accept it unconditionally
handler.ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
handler.UseCookies = true;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
}

public async Task Login() {
if (_email.Length == 0 || _password.Length == 0) {
throw new Exception("No credentials configured");
}

LoggingIn = true;

CancellationTokenSource cancelSrc = new CancellationTokenSource();
CancellationToken cancel = cancelSrc.Token;

Task timeout = Task.Delay(RequestTimeoutMs, cancel);

string loginUrl = $"https://{_ipAddress}/api/login/Basic";
Program.WriteLog(LogType.Console, loginUrl);
HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Post, loginUrl);

LoginRequest loginRequest = new LoginRequest {
email = _email,
password = _password,
username = "customer",
force_sm_off = false
};

string loginRequestString = _jsonSerializer.Serialize(loginRequest);
req.Content = new StringContent(loginRequestString, Encoding.UTF8, "application/json");
Task<HttpResponseMessage> responseTask = _httpClient.SendAsync(req, cancel);

Task finished = await Task.WhenAny(timeout, responseTask);
cancelSrc.Cancel();
if (finished == timeout) {
LoggingIn = false;
throw new Exception("Login request timed out");
}

HttpResponseMessage res = ((Task<HttpResponseMessage>) finished).Result;
string responseText = await res.Content.ReadAsStringAsync();
dynamic content = _jsonSerializer.DeserializeObject(responseText);
Program.WriteLog(LogType.Console, $"Login request complete with status {res.StatusCode}");

req.Dispose();
res.Dispose();

if (content.ContainsKey("error") && content["error"] != null) {
LoggingIn = false;
throw new Exception($"Login failed ({content["error"]})");
}

Program.WriteLog(LogType.Info, "Successfully logged into Gateway API");
LastLogin = DateTime.Now;
LoggingIn = false;
}

public async Task<SiteInfo> GetSiteInfo() {
Expand Down Expand Up @@ -83,16 +149,39 @@ public async Task<double> GetSystemChargePercentage() {
}

private async Task<dynamic> GetApiContent(string endpoint) {
if (LoggingIn) {
Program.WriteLog(LogType.Console, $"Suppressing {endpoint} because we are actively logging in.");
}

HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Get, $"https://{this._ipAddress}/api{endpoint}");
HttpResponseMessage res = await this._httpClient.SendAsync(req);
string responseText = await res.Content.ReadAsStringAsync();
dynamic content = this._jsonSerializer.DeserializeObject(responseText);
Program.WriteLog(LogType.Console, $"Request complete with status {res.StatusCode}");

if (res.StatusCode == HttpStatusCode.Forbidden) {
// We need to log in
req.Dispose();
res.Dispose();

Program.WriteLog(LogType.Console, $"Request to {endpoint} failed with status code Forbidden; attempting to login");
await Login();
return await GetApiContent(endpoint);
}

req.Dispose();
res.Dispose();
return content;
}
}

[SuppressMessage("ReSharper", "InconsistentNaming")]
internal struct LoginRequest {
public string email;
public bool force_sm_off;
public string password;
public string username;
}

public struct SiteInfo
{
Expand Down

0 comments on commit fcd00d3

Please sign in to comment.