From 2bfa90098a85944d82ffbde89da3e22cea70f110 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 18 Nov 2016 20:00:18 +0100 Subject: [PATCH 1/3] Fixed pairing, as was discussed here: https://github.com/hdurdle/harmony/issues/10 Fixed formatting, removed unneeded files. Fixed braces. --- HarmonyConsole/Program.cs | 2 +- HarmonyDemo/FormMain.Designer.cs | 207 +++++++----------- HarmonyDemo/FormMain.cs | 8 +- HarmonyHub/Entities/Auth/Credentials.cs | 24 -- .../Entities/Auth/GetUserAuthTokenResult.cs | 24 -- .../Auth/GetUserAuthTokenResultRootObject.cs | 17 -- HarmonyHub/HarmonyAuthentication.cs | 79 ------- HarmonyHub/HarmonyClient.cs | 50 ++--- HarmonyHub/HarmonyHub.csproj | 4 - HarmonyHub/Internals/HarmonyDocuments.cs | 5 +- 10 files changed, 106 insertions(+), 314 deletions(-) delete mode 100644 HarmonyHub/Entities/Auth/Credentials.cs delete mode 100644 HarmonyHub/Entities/Auth/GetUserAuthTokenResult.cs delete mode 100644 HarmonyHub/Entities/Auth/GetUserAuthTokenResultRootObject.cs delete mode 100644 HarmonyHub/HarmonyAuthentication.cs diff --git a/HarmonyConsole/Program.cs b/HarmonyConsole/Program.cs index 6fd4d1b..b997dfd 100644 --- a/HarmonyConsole/Program.cs +++ b/HarmonyConsole/Program.cs @@ -32,7 +32,7 @@ public static async Task MainAsync(string[] args) } else { - client = await HarmonyClient.Create(options.IpAddress, options.Username, options.Password); + client = await HarmonyClient.Create(options.IpAddress); File.WriteAllText("SessionToken", client.Token); } diff --git a/HarmonyDemo/FormMain.Designer.cs b/HarmonyDemo/FormMain.Designer.cs index 1c2e5d8..85d5c59 100644 --- a/HarmonyDemo/FormMain.Designer.cs +++ b/HarmonyDemo/FormMain.Designer.cs @@ -28,128 +28,93 @@ protected override void Dispose(bool disposing) /// private void InitializeComponent() { - this.textBoxHarmonyHubAddress = new System.Windows.Forms.TextBox(); - this.label1 = new System.Windows.Forms.Label(); - this.labelLogitechUserName = new System.Windows.Forms.Label(); - this.textBoxUserName = new System.Windows.Forms.TextBox(); - this.label2 = new System.Windows.Forms.Label(); - this.textBoxPassword = new System.Windows.Forms.TextBox(); - this.buttonConnect = new System.Windows.Forms.Button(); - this.treeViewConfig = new System.Windows.Forms.TreeView(); - this.statusStrip = new System.Windows.Forms.StatusStrip(); - this.toolStripStatusLabelConnection = new System.Windows.Forms.ToolStripStatusLabel(); - this.statusStrip.SuspendLayout(); - this.SuspendLayout(); - // - // textBoxHarmonyHubAddress - // - this.textBoxHarmonyHubAddress.Location = new System.Drawing.Point(35, 38); - this.textBoxHarmonyHubAddress.Name = "textBoxHarmonyHubAddress"; - this.textBoxHarmonyHubAddress.Size = new System.Drawing.Size(100, 20); - this.textBoxHarmonyHubAddress.TabIndex = 0; - this.textBoxHarmonyHubAddress.Text = "HarmonyHub"; - // - // label1 - // - this.label1.AutoSize = true; - this.label1.Location = new System.Drawing.Point(32, 22); - this.label1.Name = "label1"; - this.label1.Size = new System.Drawing.Size(116, 13); - this.label1.TabIndex = 1; - this.label1.Text = "Harmony Hub Address:"; - // - // labelLogitechUserName - // - this.labelLogitechUserName.AutoSize = true; - this.labelLogitechUserName.Location = new System.Drawing.Point(186, 22); - this.labelLogitechUserName.Name = "labelLogitechUserName"; - this.labelLogitechUserName.Size = new System.Drawing.Size(103, 13); - this.labelLogitechUserName.TabIndex = 3; - this.labelLogitechUserName.Text = "Logitech user name:"; - // - // textBoxUserName - // - this.textBoxUserName.Location = new System.Drawing.Point(189, 38); - this.textBoxUserName.Name = "textBoxUserName"; - this.textBoxUserName.Size = new System.Drawing.Size(134, 20); - this.textBoxUserName.TabIndex = 2; - this.textBoxUserName.Text = "myname@coolmail.com"; - // - // label2 - // - this.label2.AutoSize = true; - this.label2.Location = new System.Drawing.Point(350, 22); - this.label2.Name = "label2"; - this.label2.Size = new System.Drawing.Size(99, 13); - this.label2.TabIndex = 5; - this.label2.Text = "Logitech password:"; - // - // textBoxPassword - // - this.textBoxPassword.Location = new System.Drawing.Point(353, 38); - this.textBoxPassword.Name = "textBoxPassword"; - this.textBoxPassword.PasswordChar = '*'; - this.textBoxPassword.Size = new System.Drawing.Size(134, 20); - this.textBoxPassword.TabIndex = 4; - // - // buttonConnect - // - this.buttonConnect.Location = new System.Drawing.Point(12, 94); - this.buttonConnect.Name = "buttonConnect"; - this.buttonConnect.Size = new System.Drawing.Size(75, 23); - this.buttonConnect.TabIndex = 6; - this.buttonConnect.Text = "Connect"; - this.buttonConnect.UseVisualStyleBackColor = true; - this.buttonConnect.Click += new System.EventHandler(this.buttonConnect_Click); - // - // treeViewConfig - // - this.treeViewConfig.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + this.textBoxHarmonyHubAddress = new System.Windows.Forms.TextBox(); + this.label1 = new System.Windows.Forms.Label(); + this.buttonConnect = new System.Windows.Forms.Button(); + this.treeViewConfig = new System.Windows.Forms.TreeView(); + this.statusStrip = new System.Windows.Forms.StatusStrip(); + this.toolStripStatusLabelConnection = new System.Windows.Forms.ToolStripStatusLabel(); + this.statusStrip.SuspendLayout(); + this.SuspendLayout(); + // + // textBoxHarmonyHubAddress + // + this.textBoxHarmonyHubAddress.Location = new System.Drawing.Point(47, 47); + this.textBoxHarmonyHubAddress.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4); + this.textBoxHarmonyHubAddress.Name = "textBoxHarmonyHubAddress"; + this.textBoxHarmonyHubAddress.Size = new System.Drawing.Size(132, 22); + this.textBoxHarmonyHubAddress.TabIndex = 0; + this.textBoxHarmonyHubAddress.Text = "HarmonyHub"; + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(43, 27); + this.label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(155, 17); + this.label1.TabIndex = 1; + this.label1.Text = "Harmony Hub Address:"; + // + // buttonConnect + // + this.buttonConnect.Location = new System.Drawing.Point(16, 116); + this.buttonConnect.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4); + this.buttonConnect.Name = "buttonConnect"; + this.buttonConnect.Size = new System.Drawing.Size(100, 28); + this.buttonConnect.TabIndex = 6; + this.buttonConnect.Text = "Connect"; + this.buttonConnect.UseVisualStyleBackColor = true; + this.buttonConnect.Click += new System.EventHandler(this.buttonConnect_Click); + // + // treeViewConfig + // + this.treeViewConfig.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); - this.treeViewConfig.Location = new System.Drawing.Point(93, 94); - this.treeViewConfig.Name = "treeViewConfig"; - this.treeViewConfig.Size = new System.Drawing.Size(575, 450); - this.treeViewConfig.TabIndex = 7; - this.treeViewConfig.NodeMouseDoubleClick += new System.Windows.Forms.TreeNodeMouseClickEventHandler(this.treeViewConfig_NodeMouseDoubleClick); - // - // statusStrip - // - this.statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.treeViewConfig.Location = new System.Drawing.Point(124, 116); + this.treeViewConfig.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4); + this.treeViewConfig.Name = "treeViewConfig"; + this.treeViewConfig.Size = new System.Drawing.Size(765, 553); + this.treeViewConfig.TabIndex = 7; + this.treeViewConfig.NodeMouseDoubleClick += new System.Windows.Forms.TreeNodeMouseClickEventHandler(this.treeViewConfig_NodeMouseDoubleClick); + // + // statusStrip + // + this.statusStrip.ImageScalingSize = new System.Drawing.Size(20, 20); + this.statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { this.toolStripStatusLabelConnection}); - this.statusStrip.Location = new System.Drawing.Point(0, 557); - this.statusStrip.Name = "statusStrip"; - this.statusStrip.Size = new System.Drawing.Size(680, 22); - this.statusStrip.TabIndex = 8; - this.statusStrip.Text = "App Status"; - // - // toolStripStatusLabelConnection - // - this.toolStripStatusLabelConnection.Name = "toolStripStatusLabelConnection"; - this.toolStripStatusLabelConnection.Size = new System.Drawing.Size(104, 17); - this.toolStripStatusLabelConnection.Text = "Connection Status"; - // - // FormMain - // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(680, 579); - this.Controls.Add(this.statusStrip); - this.Controls.Add(this.treeViewConfig); - this.Controls.Add(this.buttonConnect); - this.Controls.Add(this.label2); - this.Controls.Add(this.textBoxPassword); - this.Controls.Add(this.labelLogitechUserName); - this.Controls.Add(this.textBoxUserName); - this.Controls.Add(this.label1); - this.Controls.Add(this.textBoxHarmonyHubAddress); - this.Name = "FormMain"; - this.Text = "Harmony Demo"; - this.Load += new System.EventHandler(this.FormMain_Load); - this.statusStrip.ResumeLayout(false); - this.statusStrip.PerformLayout(); - this.ResumeLayout(false); - this.PerformLayout(); + this.statusStrip.Location = new System.Drawing.Point(0, 688); + this.statusStrip.Name = "statusStrip"; + this.statusStrip.Padding = new System.Windows.Forms.Padding(1, 0, 19, 0); + this.statusStrip.Size = new System.Drawing.Size(907, 25); + this.statusStrip.TabIndex = 8; + this.statusStrip.Text = "App Status"; + // + // toolStripStatusLabelConnection + // + this.toolStripStatusLabelConnection.Name = "toolStripStatusLabelConnection"; + this.toolStripStatusLabelConnection.Size = new System.Drawing.Size(128, 20); + this.toolStripStatusLabelConnection.Text = "Connection Status"; + // + // FormMain + // + this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(907, 713); + this.Controls.Add(this.statusStrip); + this.Controls.Add(this.treeViewConfig); + this.Controls.Add(this.buttonConnect); + this.Controls.Add(this.label1); + this.Controls.Add(this.textBoxHarmonyHubAddress); + this.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4); + this.Name = "FormMain"; + this.Text = "Harmony Demo"; + this.Load += new System.EventHandler(this.FormMain_Load); + this.statusStrip.ResumeLayout(false); + this.statusStrip.PerformLayout(); + this.ResumeLayout(false); + this.PerformLayout(); } @@ -157,10 +122,6 @@ private void InitializeComponent() private System.Windows.Forms.TextBox textBoxHarmonyHubAddress; private System.Windows.Forms.Label label1; - private System.Windows.Forms.Label labelLogitechUserName; - private System.Windows.Forms.TextBox textBoxUserName; - private System.Windows.Forms.Label label2; - private System.Windows.Forms.TextBox textBoxPassword; private System.Windows.Forms.Button buttonConnect; private System.Windows.Forms.TreeView treeViewConfig; private System.Windows.Forms.StatusStrip statusStrip; diff --git a/HarmonyDemo/FormMain.cs b/HarmonyDemo/FormMain.cs index 62c4e8b..fc3ff50 100644 --- a/HarmonyDemo/FormMain.cs +++ b/HarmonyDemo/FormMain.cs @@ -60,14 +60,8 @@ private async Task ConnectAsync() } else { - if (string.IsNullOrEmpty(textBoxPassword.Text)) - { - toolStripStatusLabelConnection.Text = "Credentials missing!"; - return; - } - toolStripStatusLabelConnection.Text += "authenticating with Logitech servers..."; - Program.Client = await HarmonyClient.Create(textBoxHarmonyHubAddress.Text, textBoxUserName.Text, textBoxPassword.Text); + Program.Client = await HarmonyClient.Create(textBoxHarmonyHubAddress.Text); File.WriteAllText("SessionToken", Program.Client.Token); } diff --git a/HarmonyHub/Entities/Auth/Credentials.cs b/HarmonyHub/Entities/Auth/Credentials.cs deleted file mode 100644 index d2f69f7..0000000 --- a/HarmonyHub/Entities/Auth/Credentials.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Runtime.Serialization; - -namespace HarmonyHub.Entities.Auth -{ - /// - /// Credentials for the Authentication process between the client and the MyHarmony site - /// - [DataContract] - public class Credentials - { - /// - /// Username at MyHarmony, usualls the email adress - /// - [DataMember(Name = "email")] - public string Username { get; set; } - - /// - /// The password at MyHarmony - /// - [DataMember(Name = "password")] - public string Password { get; set; } - } - -} diff --git a/HarmonyHub/Entities/Auth/GetUserAuthTokenResult.cs b/HarmonyHub/Entities/Auth/GetUserAuthTokenResult.cs deleted file mode 100644 index 565269b..0000000 --- a/HarmonyHub/Entities/Auth/GetUserAuthTokenResult.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Runtime.Serialization; - -namespace HarmonyHub.Entities.Auth -{ - /// - /// Result of call to myharmony.com web service. - /// AccountId is always (so far) 0. - /// - [DataContract] - public class GetUserAuthTokenResult - { - /// - /// ID of the account - /// - [DataMember(Name = "AccountId")] - public int AccountId { get; set; } - - /// - /// Auth-Token for accessing the HarmonyHub, this can be used to get an acces token. - /// - [DataMember(Name = "UserAuthToken")] - public string UserAuthToken { get; set; } - } -} \ No newline at end of file diff --git a/HarmonyHub/Entities/Auth/GetUserAuthTokenResultRootObject.cs b/HarmonyHub/Entities/Auth/GetUserAuthTokenResultRootObject.cs deleted file mode 100644 index 04b7ad5..0000000 --- a/HarmonyHub/Entities/Auth/GetUserAuthTokenResultRootObject.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Runtime.Serialization; - -namespace HarmonyHub.Entities.Auth -{ - /// - /// Root object for the authentication json object which is returned from the MyHarmony site - /// - [DataContract] - public class GetUserAuthTokenResultRootObject - { - /// - /// Contains the result - /// - [DataMember(Name = "GetUserAuthTokenResult")] - public GetUserAuthTokenResult GetUserAuthTokenResult { get; set; } - } -} \ No newline at end of file diff --git a/HarmonyHub/HarmonyAuthentication.cs b/HarmonyHub/HarmonyAuthentication.cs deleted file mode 100644 index 44b525b..0000000 --- a/HarmonyHub/HarmonyAuthentication.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using HarmonyHub.Entities.Auth; -using HarmonyHub.Utils; - -namespace HarmonyHub -{ - /// - /// This class handles the Http communication with Logitech - /// - public static class HarmonyAuthentication - { - /// - /// This is the Url to the logitech service myharmony, which can get the user authentication for Harmony-hubs. - /// - public const string LogitechAuthUrl = "https://svcs.myharmony.com/CompositeSecurityServices/Security.svc/json/GetUserAuthToken"; - - /// - /// Logs in to the Logitech Harmony web service to get a UserAuthToken. - /// - /// myharmony.com username - /// myharmony.com password - /// CancellationToken - /// Logitech UserAuthToken - public static async Task GetUserAuthToken(string username, string password, CancellationToken cancellationToken = default(CancellationToken)) - { - // Get the default proxy, and configure it - var proxyToUse = WebRequest.GetSystemWebProxy(); - if (proxyToUse is WebProxy) - { - // Read note here: https://msdn.microsoft.com/en-us/library/system.net.webproxy.credentials.aspx - var webProxy = proxyToUse as WebProxy; - webProxy.UseDefaultCredentials = true; - } - else - { - proxyToUse.Credentials = CredentialCache.DefaultCredentials; - } - - // Create a HttpClientHandler, and a HttpClient, in a using so they are disposed - using (var httpClientHandler = new HttpClientHandler() { UseProxy = true, Proxy = proxyToUse}) - using (var httpClient = new HttpClient(httpClientHandler)) - { - // Configure the httpClient for json and don't expect a continue - httpClient.DefaultRequestHeaders.ExpectContinue = false; - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - - // Prepare the json string - var credentialsJson = Serializer.ToJson(new Credentials - { - Username = username, - Password = password - }); - // Create a HttpContent for the string - var jsonContent = new StringContent(credentialsJson, Encoding.UTF8); - - // Post and get the reponse message - var httpResponseMessage = await httpClient.PostAsync(new Uri(LogitechAuthUrl), jsonContent, cancellationToken); - - // Ensure we got a succes, if not this will throw - httpResponseMessage.EnsureSuccessStatusCode(); - - // Get the result - var result = await httpResponseMessage.Content.ReadAsStringAsync(); - - // Deserialize the result - var harmonyData = Serializer.FromJson(result); - - // Return the part that we need - return harmonyData.GetUserAuthTokenResult.UserAuthToken; - } - } - } -} diff --git a/HarmonyHub/HarmonyClient.cs b/HarmonyHub/HarmonyClient.cs index 9f7b799..654ef91 100644 --- a/HarmonyHub/HarmonyClient.cs +++ b/HarmonyHub/HarmonyClient.cs @@ -2,7 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; -using System.Text; +using System.Threading; using System.Threading.Tasks; using agsXMPP; using agsXMPP.protocol.client; @@ -11,7 +11,6 @@ using HarmonyHub.Entities.Response; using HarmonyHub.Internals; using HarmonyHub.Utils; -using System.Threading; namespace HarmonyHub { @@ -33,11 +32,6 @@ public class HarmonyClient : IDisposable // The connection private readonly XmppClientConnection _xmpp; - /// - /// This event is triggered when the current activity is changed - /// - public event EventHandler OnActivityChanged; - /// /// Constructor with standard settings for a new HarmonyClient /// @@ -101,28 +95,20 @@ public static HarmonyClient Create(string host, string token, int port = 5222) /// Create a harmony client via myharmony.com authenification /// /// IP or hostname - /// myharmony.com username (email) - /// myharmony.com password /// Port to connect to, default 5222 /// HarmonyClient - public static async Task Create(string host, string username, string password, int port = 5222) + public static async Task Create(string host, int port = 5222) { - string userAuthToken = await HarmonyAuthentication.GetUserAuthToken(username, password); - if (string.IsNullOrEmpty(userAuthToken)) - { - throw new Exception("Could not get token from Logitech server."); - } - // Make a guest connection only to exchange the session token via the user authentication token string sessionToken; using (var client = new HarmonyClient(host, "guest", port)) { - sessionToken = await client.SwapAuthToken(userAuthToken).ConfigureAwait(false); + sessionToken = await client.CreateToken().ConfigureAwait(false); } if (string.IsNullOrEmpty(sessionToken)) { - throw new Exception("Could not swap token on Harmony Hub."); + throw new Exception("Could not get token from Harmony Hub."); } // Create the client with the session token @@ -161,7 +147,6 @@ private async Task FireAndForgetAsync(Document document, int waitTimeout = 50) // This makes sure the exception, if there was one, is unwrapped await task; - } /// @@ -209,6 +194,11 @@ private string GetData(IQ iq) return null; } + /// + /// This event is triggered when the current activity is changed + /// + public event EventHandler OnActivityChanged; + /// /// Send a document, await the response and return it @@ -239,7 +229,6 @@ private async Task RequestResponseAsync(Document document, int timeout = 200 _resultTaskCompletionSources.Remove(iqToSend.Id); // Pass the timeout exception to the await resultTaskCompletionSource.TrySetException(new TimeoutException($"Timeout while waiting on response {iqToSend.Id} after {timeout}")); - }; // Start the sending @@ -257,18 +246,15 @@ private async Task RequestResponseAsync(Document document, int timeout = 200 #region Authentication /// - /// Send message to HarmonyHub with UserAuthToken, wait for SessionToken + /// Send message to HarmonyHub, wait for SessionToken /// - /// - /// - public async Task SwapAuthToken(string userAuthToken) + /// session token + public async Task CreateToken() { - var iq = await RequestResponseAsync(HarmonyDocuments.LogitechPairDocument(userAuthToken)).ConfigureAwait(false); + var iq = await RequestResponseAsync(HarmonyDocuments.LogitechPairDocument()).ConfigureAwait(false); var sessionData = GetData(iq); - if (sessionData != null) - { - foreach (var pair in sessionData.Split(':')) - { + if (sessionData != null) { + foreach (var pair in sessionData.Split(':')) { if (pair.StartsWith("identity")) { return pair.Split('=')[1]; @@ -341,7 +327,7 @@ private void OnIqResponseHandler(object sender, IQ iq) Debug.WriteLine("Received event " + iq.Id); Debug.WriteLine(iq.ToString()); TaskCompletionSource resulTaskCompletionSource; - if (iq.Id != null && _resultTaskCompletionSources.TryGetValue(iq.Id, out resulTaskCompletionSource)) + if ((iq.Id != null) && _resultTaskCompletionSources.TryGetValue(iq.Id, out resulTaskCompletionSource)) { // Error handling from XMPP if (iq.Error != null) @@ -485,8 +471,8 @@ public async Task SendCommandAsync(string deviceId, string command, bool press = /// The time between the press and release, default 100ms public async Task SendKeyPressAsync(string deviceId, string command, int timespan = 100) { - var now = (int)DateTime.Now.ToUniversalTime().Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds; - var press = HarmonyDocuments.IrCommandDocument(deviceId, command, true, now -timespan); + var now = (int) DateTime.Now.ToUniversalTime().Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds; + var press = HarmonyDocuments.IrCommandDocument(deviceId, command, true, now - timespan); await FireAndForgetAsync(press).ConfigureAwait(false); var release = HarmonyDocuments.IrCommandDocument(deviceId, command, false, timespan); await FireAndForgetAsync(release).ConfigureAwait(false); diff --git a/HarmonyHub/HarmonyHub.csproj b/HarmonyHub/HarmonyHub.csproj index 4b457a5..e9356c6 100644 --- a/HarmonyHub/HarmonyHub.csproj +++ b/HarmonyHub/HarmonyHub.csproj @@ -48,18 +48,14 @@ - - - - diff --git a/HarmonyHub/Internals/HarmonyDocuments.cs b/HarmonyHub/Internals/HarmonyDocuments.cs index 3bd3be7..74d3020 100644 --- a/HarmonyHub/Internals/HarmonyDocuments.cs +++ b/HarmonyHub/Internals/HarmonyDocuments.cs @@ -107,9 +107,8 @@ public static Document IrCommandDocument(string deviceId, string command, bool p /// /// Create a "pair" document, this is a bit different from the others /// - /// Token /// Document - public static Document LogitechPairDocument(string token) + public static Document LogitechPairDocument() { var document = new Document { @@ -119,7 +118,7 @@ public static Document LogitechPairDocument(string token) var element = new Element("oa"); element.Attributes.Add("xmlns", "connect.logitech.com"); element.Attributes.Add("mime", "vnd.logitech.connect/vnd.logitech.pair"); - element.Value = $"token={token}:name=foo#iOS6.0.1#iPhone"; + element.Value = "method=pair:name=foo#iOS6.0.1#iPhone"; document.AddChild(element); return document; } From 7cee0cb22f05df460930828b279b6cee5c95f150 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 18 Nov 2016 21:03:43 +0100 Subject: [PATCH 2/3] Removed username and password. --- HarmonyDemo/FormMain.Designer.cs | 88 +++++++------------------------- 1 file changed, 19 insertions(+), 69 deletions(-) diff --git a/HarmonyDemo/FormMain.Designer.cs b/HarmonyDemo/FormMain.Designer.cs index 9b2112e..64d1bc8 100644 --- a/HarmonyDemo/FormMain.Designer.cs +++ b/HarmonyDemo/FormMain.Designer.cs @@ -30,10 +30,6 @@ private void InitializeComponent() { this.textBoxHarmonyHubAddress = new System.Windows.Forms.TextBox(); this.label1 = new System.Windows.Forms.Label(); - this.labelLogitechUserName = new System.Windows.Forms.Label(); - this.textBoxUserName = new System.Windows.Forms.TextBox(); - this.label2 = new System.Windows.Forms.Label(); - this.textBoxPassword = new System.Windows.Forms.TextBox(); this.buttonConnect = new System.Windows.Forms.Button(); this.treeViewConfig = new System.Windows.Forms.TreeView(); this.statusStrip = new System.Windows.Forms.StatusStrip(); @@ -43,67 +39,29 @@ private void InitializeComponent() // // textBoxHarmonyHubAddress // - this.textBoxHarmonyHubAddress.Location = new System.Drawing.Point(70, 73); - this.textBoxHarmonyHubAddress.Margin = new System.Windows.Forms.Padding(6, 6, 6, 6); + this.textBoxHarmonyHubAddress.Location = new System.Drawing.Point(47, 47); + this.textBoxHarmonyHubAddress.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4); this.textBoxHarmonyHubAddress.Name = "textBoxHarmonyHubAddress"; - this.textBoxHarmonyHubAddress.Size = new System.Drawing.Size(196, 31); + this.textBoxHarmonyHubAddress.Size = new System.Drawing.Size(132, 22); this.textBoxHarmonyHubAddress.TabIndex = 0; this.textBoxHarmonyHubAddress.Text = "HarmonyHub"; // // label1 // this.label1.AutoSize = true; - this.label1.Location = new System.Drawing.Point(64, 42); - this.label1.Margin = new System.Windows.Forms.Padding(6, 0, 6, 0); + this.label1.Location = new System.Drawing.Point(43, 27); + this.label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); this.label1.Name = "label1"; - this.label1.Size = new System.Drawing.Size(234, 25); + this.label1.Size = new System.Drawing.Size(155, 17); this.label1.TabIndex = 1; this.label1.Text = "Harmony Hub Address:"; // - // labelLogitechUserName - // - this.labelLogitechUserName.AutoSize = true; - this.labelLogitechUserName.Location = new System.Drawing.Point(372, 42); - this.labelLogitechUserName.Margin = new System.Windows.Forms.Padding(6, 0, 6, 0); - this.labelLogitechUserName.Name = "labelLogitechUserName"; - this.labelLogitechUserName.Size = new System.Drawing.Size(207, 25); - this.labelLogitechUserName.TabIndex = 3; - this.labelLogitechUserName.Text = "Logitech user name:"; - // - // textBoxUserName - // - this.textBoxUserName.Location = new System.Drawing.Point(378, 73); - this.textBoxUserName.Margin = new System.Windows.Forms.Padding(6, 6, 6, 6); - this.textBoxUserName.Name = "textBoxUserName"; - this.textBoxUserName.Size = new System.Drawing.Size(264, 31); - this.textBoxUserName.TabIndex = 2; - this.textBoxUserName.Text = "myname@coolmail.com"; - // - // label2 - // - this.label2.AutoSize = true; - this.label2.Location = new System.Drawing.Point(700, 42); - this.label2.Margin = new System.Windows.Forms.Padding(6, 0, 6, 0); - this.label2.Name = "label2"; - this.label2.Size = new System.Drawing.Size(198, 25); - this.label2.TabIndex = 5; - this.label2.Text = "Logitech password:"; - // - // textBoxPassword - // - this.textBoxPassword.Location = new System.Drawing.Point(706, 73); - this.textBoxPassword.Margin = new System.Windows.Forms.Padding(6, 6, 6, 6); - this.textBoxPassword.Name = "textBoxPassword"; - this.textBoxPassword.PasswordChar = '*'; - this.textBoxPassword.Size = new System.Drawing.Size(264, 31); - this.textBoxPassword.TabIndex = 4; - // // buttonConnect // - this.buttonConnect.Location = new System.Drawing.Point(24, 181); - this.buttonConnect.Margin = new System.Windows.Forms.Padding(6, 6, 6, 6); + this.buttonConnect.Location = new System.Drawing.Point(16, 116); + this.buttonConnect.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4); this.buttonConnect.Name = "buttonConnect"; - this.buttonConnect.Size = new System.Drawing.Size(150, 44); + this.buttonConnect.Size = new System.Drawing.Size(100, 28); this.buttonConnect.TabIndex = 6; this.buttonConnect.Text = "Connect"; this.buttonConnect.UseVisualStyleBackColor = true; @@ -114,10 +72,10 @@ private void InitializeComponent() this.treeViewConfig.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); - this.treeViewConfig.Location = new System.Drawing.Point(186, 181); - this.treeViewConfig.Margin = new System.Windows.Forms.Padding(6, 6, 6, 6); + this.treeViewConfig.Location = new System.Drawing.Point(124, 116); + this.treeViewConfig.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4); this.treeViewConfig.Name = "treeViewConfig"; - this.treeViewConfig.Size = new System.Drawing.Size(1146, 862); + this.treeViewConfig.Size = new System.Drawing.Size(765, 553); this.treeViewConfig.TabIndex = 7; this.treeViewConfig.NodeMouseDoubleClick += new System.Windows.Forms.TreeNodeMouseClickEventHandler(this.treeViewConfig_NodeMouseDoubleClick); // @@ -126,34 +84,30 @@ private void InitializeComponent() this.statusStrip.ImageScalingSize = new System.Drawing.Size(32, 32); this.statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { this.toolStripStatusLabelConnection}); - this.statusStrip.Location = new System.Drawing.Point(0, 1076); + this.statusStrip.Location = new System.Drawing.Point(0, 644); this.statusStrip.Name = "statusStrip"; - this.statusStrip.Padding = new System.Windows.Forms.Padding(2, 0, 28, 0); - this.statusStrip.Size = new System.Drawing.Size(1360, 37); + this.statusStrip.Padding = new System.Windows.Forms.Padding(1, 0, 19, 0); + this.statusStrip.Size = new System.Drawing.Size(907, 25); this.statusStrip.TabIndex = 8; this.statusStrip.Text = "App Status"; // // toolStripStatusLabelConnection // this.toolStripStatusLabelConnection.Name = "toolStripStatusLabelConnection"; - this.toolStripStatusLabelConnection.Size = new System.Drawing.Size(209, 32); + this.toolStripStatusLabelConnection.Size = new System.Drawing.Size(128, 20); this.toolStripStatusLabelConnection.Text = "Connection Status"; // // FormMain // - this.AutoScaleDimensions = new System.Drawing.SizeF(12F, 25F); + this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(1360, 1113); + this.ClientSize = new System.Drawing.Size(907, 669); this.Controls.Add(this.statusStrip); this.Controls.Add(this.treeViewConfig); this.Controls.Add(this.buttonConnect); - this.Controls.Add(this.label2); - this.Controls.Add(this.textBoxPassword); - this.Controls.Add(this.labelLogitechUserName); - this.Controls.Add(this.textBoxUserName); this.Controls.Add(this.label1); this.Controls.Add(this.textBoxHarmonyHubAddress); - this.Margin = new System.Windows.Forms.Padding(6, 6, 6, 6); + this.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4); this.Name = "FormMain"; this.Text = "Harmony Demo"; this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.FormMain_FormClosing); @@ -169,10 +123,6 @@ private void InitializeComponent() private System.Windows.Forms.TextBox textBoxHarmonyHubAddress; private System.Windows.Forms.Label label1; - private System.Windows.Forms.Label labelLogitechUserName; - private System.Windows.Forms.TextBox textBoxUserName; - private System.Windows.Forms.Label label2; - private System.Windows.Forms.TextBox textBoxPassword; private System.Windows.Forms.Button buttonConnect; private System.Windows.Forms.TreeView treeViewConfig; private System.Windows.Forms.StatusStrip statusStrip; From 8d5038ca35f295324fadf8093336e50eb2a364a4 Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 6 Feb 2017 19:55:36 +0100 Subject: [PATCH 3/3] Changes for #19 --- HarmonyHub/HarmonyClient.cs | 967 ++++++++++++++++++------------------ 1 file changed, 489 insertions(+), 478 deletions(-) diff --git a/HarmonyHub/HarmonyClient.cs b/HarmonyHub/HarmonyClient.cs index 654ef91..44ad8fb 100644 --- a/HarmonyHub/HarmonyClient.cs +++ b/HarmonyHub/HarmonyClient.cs @@ -14,482 +14,493 @@ namespace HarmonyHub { - /// - /// Client to interrogate and control Logitech Harmony Hub. - /// - public class HarmonyClient : IDisposable - { - /// - /// This has the login state.. - /// When the OnLoginHandler is triggered this is set with true, - /// When an error occurs before this, the expeception is set. - /// Everywhere where this is awaited the state is returned, but blocks until there is something. - /// - private readonly TaskCompletionSource _loginTaskCompletionSource = new TaskCompletionSource(); - - // A lookup to correlate request and responses - private readonly IDictionary> _resultTaskCompletionSources = new ConcurrentDictionary>(); - // The connection - private readonly XmppClientConnection _xmpp; - - /// - /// Constructor with standard settings for a new HarmonyClient - /// - /// IP or hostname - /// Auth-token, or guest - /// The port to connect to, default 5222 - private HarmonyClient(string host, string token, int port = 5222) - { - Token = token; - _xmpp = new XmppClientConnection(host, port) - { - UseStartTLS = false, - UseSSL = false, - UseCompression = false, - AutoResolveConnectServer = false, - AutoAgents = false, - AutoPresence = true, - AutoRoster = true - }; - // Configure Sasl not to use auto and PLAIN for authentication - _xmpp.OnSaslStart += SaslStartHandler; - _xmpp.OnLogin += OnLoginHandler; - _xmpp.OnIq += OnIqResponseHandler; - _xmpp.OnMessage += OnMessage; - _xmpp.OnSocketError += ErrorHandler; - // Open the connection, do the login - _xmpp.Open($"{token}@x.com", token); - } - - /// - /// Read the token used for the connection, maybe to store it and use it another time. - /// - public string Token { get; private set; } - - /// - /// Cleanup and close - /// - public void Dispose() - { - _xmpp.OnIq -= OnIqResponseHandler; - _xmpp.OnMessage -= OnMessage; - _xmpp.OnLogin -= OnLoginHandler; - _xmpp.OnSocketError -= ErrorHandler; - _xmpp.OnSaslStart -= SaslStartHandler; - _xmpp.Close(); - } - - /// - /// Create a HarmonyClient with pre-authenticated token - /// - /// IP or hostname - /// token which is created via an authentication via myharmony.com - /// Port to connect to, 5222 is the default - /// - public static HarmonyClient Create(string host, string token, int port = 5222) - { - return new HarmonyClient(host, token, port); - } - - /// - /// Create a harmony client via myharmony.com authenification - /// - /// IP or hostname - /// Port to connect to, default 5222 - /// HarmonyClient - public static async Task Create(string host, int port = 5222) - { - // Make a guest connection only to exchange the session token via the user authentication token - string sessionToken; - using (var client = new HarmonyClient(host, "guest", port)) - { - sessionToken = await client.CreateToken().ConfigureAwait(false); - } - - if (string.IsNullOrEmpty(sessionToken)) - { - throw new Exception("Could not get token from Harmony Hub."); - } - - // Create the client with the session token - return new HarmonyClient(host, sessionToken, port); - } - - /// - /// Send a document, ignore the response (but wait shortly for a possible error) - /// - /// Document - /// the time to wait for a possible error, if this is too small errors are ignored. - /// Task to await on - private async Task FireAndForgetAsync(Document document, int waitTimeout = 50) - { - // Check if the login was made, this blocks until there is a state - // And throws an exception if the login failed. - await _loginTaskCompletionSource.Task.ConfigureAwait(false); - - // Create the IQ to send - var iqToSend = GenerateIq(document); - - // Prepate the TaskCompletionSource, which is used to await the result - var resultTaskCompletionSource = new TaskCompletionSource(); - _resultTaskCompletionSources[iqToSend.Id] = resultTaskCompletionSource; - - Debug.WriteLine("Sending (ignoring response):"); - Debug.WriteLine(iqToSend.ToString()); - // Start the sending - _xmpp.Send(iqToSend); - - // Await, to make sure there wasn't an error - var task = await Task.WhenAny(resultTaskCompletionSource.Task, Task.Delay(waitTimeout)).ConfigureAwait(false); - - // Remove the result task, as we no longer need it. - _resultTaskCompletionSources.Remove(iqToSend.Id); - - // This makes sure the exception, if there was one, is unwrapped - await task; - } - - /// - /// Generate an IQ for the supplied Document - /// - /// Document - /// IQ - private static IQ GenerateIq(Document document) - { - // Create the IQ to send - var iqToSend = new IQ - { - Type = IqType.get, - Namespace = "", - From = "1", - To = "guest" - }; - - // Add the real content for the Harmony - iqToSend.AddChild(document); - - // Generate an unique ID, this is used to correlate the reply to the request - iqToSend.GenerateId(); - return iqToSend; - } - - /// - /// Get the data from the IQ response object - /// - /// IQ response object - /// string with the data of the element - private string GetData(IQ iq) - { - if (iq.HasTag("oa")) - { - var oaElement = iq.SelectSingleElement("oa"); - // Keep receiving messages until we get a 200 status - // Activity commands send 100 (continue) until they finish - var errorCode = oaElement.GetAttribute("errorcode"); - if ("200".Equals(errorCode)) - { - return oaElement.GetData(); - } - } - return null; - } - - /// - /// This event is triggered when the current activity is changed - /// - public event EventHandler OnActivityChanged; - - - /// - /// Send a document, await the response and return it - /// - /// Document - /// Timeout for waiting on the response, if this passes a timeout exception is thrown - /// IQ response - private async Task RequestResponseAsync(Document document, int timeout = 2000) - { - // Check if the login was made, this blocks until there is a state - // And throws an exception if the login failed. - await _loginTaskCompletionSource.Task.ConfigureAwait(false); - - // Create the IQ to send - var iqToSend = GenerateIq(document); - - // Prepate the TaskCompletionSource, which is used to await the result - var resultTaskCompletionSource = new TaskCompletionSource(); - _resultTaskCompletionSources[iqToSend.Id] = resultTaskCompletionSource; - - Debug.WriteLine("Sending:"); - Debug.WriteLine(iqToSend.ToString()); - - // Create the action which is called when a timeout occurs - Action timeoutAction = () => - { - // Remove the registration, it is no longer needed - _resultTaskCompletionSources.Remove(iqToSend.Id); - // Pass the timeout exception to the await - resultTaskCompletionSource.TrySetException(new TimeoutException($"Timeout while waiting on response {iqToSend.Id} after {timeout}")); - }; - - // Start the sending - _xmpp.Send(iqToSend); - - // Setup the timeout handling - var cancellationTokenSource = new CancellationTokenSource(timeout); - using (cancellationTokenSource.Token.Register(timeoutAction)) - { - // Await / block until an reply arrives or the timeout happens - return await resultTaskCompletionSource.Task.ConfigureAwait(false); - } - } - - #region Authentication - - /// - /// Send message to HarmonyHub, wait for SessionToken - /// - /// session token - public async Task CreateToken() - { - var iq = await RequestResponseAsync(HarmonyDocuments.LogitechPairDocument()).ConfigureAwait(false); - var sessionData = GetData(iq); - if (sessionData != null) { - foreach (var pair in sessionData.Split(':')) { - if (pair.StartsWith("identity")) - { - return pair.Split('=')[1]; - } - } - } - throw new Exception("Wrong data"); - } - - #endregion - - #region Event Handlers - - /// - /// Handle incomming messages - /// - /// - /// - private void OnMessage(object sender, Message message) - { - if (!message.HasTag("event")) - { - return; - } - // Check for the activity changed data, see here: https://github.com/swissmanu/harmonyhubjs-client/blob/master/docs/protocol/startActivityFinished.md - var eventElement = message.SelectSingleElement("event"); - var eventData = eventElement.GetData(); - if (eventData == null) - { - return; - } - foreach (var pair in eventData.Split(':')) - { - if (!pair.StartsWith("activityId")) - { - continue; - } - var activityId = pair.Split('=')[1]; - OnActivityChanged?.Invoke(this, activityId); - } - } - - /// - /// Configure Sasl not to use auto and PLAIN for authentication - /// - /// object - /// SaslEventArgs - private void SaslStartHandler(object sender, SaslEventArgs saslEventArgs) - { - saslEventArgs.Auto = false; - saslEventArgs.Mechanism = "PLAIN"; - } - - /// - /// Handle login by completing the _loginTaskCompletionSource - /// - /// - private void OnLoginHandler(object sender) - { - _loginTaskCompletionSource.TrySetResult(true); - } - - /// - /// Lookup the TaskCompletionSource for the IQ message and try to set the result. - /// - /// object - /// IQ - private void OnIqResponseHandler(object sender, IQ iq) - { - Debug.WriteLine("Received event " + iq.Id); - Debug.WriteLine(iq.ToString()); - TaskCompletionSource resulTaskCompletionSource; - if ((iq.Id != null) && _resultTaskCompletionSources.TryGetValue(iq.Id, out resulTaskCompletionSource)) - { - // Error handling from XMPP - if (iq.Error != null) - { - var errorMessage = iq.Error.ErrorText; - Debug.WriteLine(errorMessage); - resulTaskCompletionSource.TrySetException(new Exception(errorMessage)); - // Result task is longer needed in the lookup - _resultTaskCompletionSources.Remove(iq.Id); - return; - } - - // Message processing (error handling) - if (iq.HasTag("oa")) - { - var oaElement = iq.SelectSingleElement("oa"); - - // Check error code - var errorCode = oaElement.GetAttribute("errorcode"); - // 100 -> continue - if ("100".Equals(errorCode)) - { - // Ignoring 100 continue - Debug.WriteLine("Ignoring, expecting more to come."); - - // TODO: Insert code to handle progress updates for the startActivity - } - // 200 -> OK - else if ("200".Equals(errorCode)) - { - resulTaskCompletionSource.TrySetResult(iq); - - // Result task is longer needed in the lookup - _resultTaskCompletionSources.Remove(iq.Id); - } - else - { - // We didn't get a 100 or 200, this must mean there was an error - var errorMessage = oaElement.GetAttribute("errorstring"); - Debug.WriteLine(errorMessage); - // Set the exception on the TaskCompletionSource, it will be picked up in the await - resulTaskCompletionSource.TrySetException(new Exception(errorMessage)); - - // Result task is longer needed in the lookup - _resultTaskCompletionSources.Remove(iq.Id); - } - } - else - { - Debug.WriteLine("Unexpected content"); - } - } - else - { - Debug.WriteLine("No matching result task found."); - } - } - - /// - /// Help with login errors - /// - /// object - /// Exception - private void ErrorHandler(object sender, Exception ex) - { - if (_loginTaskCompletionSource.Task.Status == TaskStatus.Created) - { - _loginTaskCompletionSource.TrySetException(ex); - } - else - { - Debug.WriteLine(ex.ToString()); - } - } - - #endregion - - #region Send Messages to HarmonyHub - - /// - /// Request the configuration from the hub - /// - /// HarmonyConfig - public async Task GetConfigAsync() - { - var iq = await RequestResponseAsync(HarmonyDocuments.ConfigDocument()).ConfigureAwait(false); - var config = GetData(iq); - if (config != null) - { - return Serializer.FromJson(config); - } - throw new Exception("No data found"); - } - - /// - /// Send message to HarmonyHub to start a given activity - /// Result is parsed by OnIq based on ClientCommandType - /// - /// string - public async Task StartActivityAsync(string activityId) - { - await RequestResponseAsync(HarmonyDocuments.StartActivityDocument(activityId)).ConfigureAwait(false); - } - - /// - /// Send message to HarmonyHub to request current activity - /// Result is parsed by OnIq based on ClientCommandType - /// - /// string with the current activity - public async Task GetCurrentActivityAsync() - { - var iq = await RequestResponseAsync(HarmonyDocuments.GetCurrentActivityDocument()).ConfigureAwait(false); - var currentActivityData = GetData(iq); - if (currentActivityData != null) - { - return currentActivityData.Split('=')[1]; - } - throw new Exception("No data found in IQ"); - } - - /// - /// Send message to HarmonyHub to request to press a button - /// Result is parsed by OnIq based on ClientCommandType - /// - /// string with the ID of the device - /// string with the command for the device - /// true for press, false for release - /// Timestamp for the command, e.g. send a press with 0 and a release with 100 - public async Task SendCommandAsync(string deviceId, string command, bool press = true, int? timestamp = null) - { - var document = HarmonyDocuments.IrCommandDocument(deviceId, command, press, timestamp); - await FireAndForgetAsync(document).ConfigureAwait(false); - } - - /// - /// Send a message that a button was pressed - /// Result is parsed by OnIq based on ClientCommandType - /// - /// string with the ID of the device - /// string with the command for the device - /// The time between the press and release, default 100ms - public async Task SendKeyPressAsync(string deviceId, string command, int timespan = 100) - { - var now = (int) DateTime.Now.ToUniversalTime().Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds; - var press = HarmonyDocuments.IrCommandDocument(deviceId, command, true, now - timespan); - await FireAndForgetAsync(press).ConfigureAwait(false); - var release = HarmonyDocuments.IrCommandDocument(deviceId, command, false, timespan); - await FireAndForgetAsync(release).ConfigureAwait(false); - } - - /// - /// Send message to HarmonyHub to request to turn off all devices - /// - public async Task TurnOffAsync() - { - var currentActivity = await GetCurrentActivityAsync().ConfigureAwait(false); - if (currentActivity != "-1") - { - await StartActivityAsync("-1").ConfigureAwait(false); - } - } - - #endregion - } + /// + /// Client to interrogate and control Logitech Harmony Hub. + /// + public class HarmonyClient : IDisposable + { + /// + /// This has the login state.. + /// When the OnLoginHandler is triggered this is set with true, + /// When an error occurs before this, the expeception is set. + /// Everywhere where this is awaited the state is returned, but blocks until there is something. + /// + private readonly TaskCompletionSource _loginTaskCompletionSource = new TaskCompletionSource(); + + // A lookup to correlate request and responses + private readonly IDictionary> _resultTaskCompletionSources = new ConcurrentDictionary>(); + // The connection + private readonly XmppClientConnection _xmpp; + + /// + /// Constructor with standard settings for a new HarmonyClient + /// + /// IP or hostname + /// Auth-token, or guest + /// The port to connect to, default 5222 + /// an optional keep alive interval, default is 50, Harmony needs 60 seconds + private HarmonyClient(string host, string token, int port = 5222, int? keepAliveInterval = 50) + { + Token = token; + _xmpp = new XmppClientConnection(host, port) + { + UseStartTLS = false, + UseSSL = false, + UseCompression = false, + AutoResolveConnectServer = false, + AutoAgents = false, + AutoPresence = true, + AutoRoster = true, + // For https://github.com/Lakritzator/harmony/issues/19 + KeepAliveInterval = keepAliveInterval ?? 120, // 120 is the underlying default + KeepAlive = keepAliveInterval.HasValue + }; + // Configure Sasl not to use auto and PLAIN for authentication + _xmpp.OnSaslStart += SaslStartHandler; + _xmpp.OnLogin += OnLoginHandler; + _xmpp.OnIq += OnIqResponseHandler; + _xmpp.OnMessage += OnMessage; + _xmpp.OnSocketError += ErrorHandler; + // Inform anyone who it may concern + _xmpp.OnClose += sender => OnConnectionClosed?.Invoke(this, EventArgs.Empty); + + // Open the connection, do the login + _xmpp.Open($"{token}@x.com", token); + } + + /// + /// Read the token used for the connection, maybe to store it and use it another time. + /// + public string Token { get; private set; } + + /// + /// Cleanup and close + /// + public void Dispose() + { + _xmpp.OnIq -= OnIqResponseHandler; + _xmpp.OnMessage -= OnMessage; + _xmpp.OnLogin -= OnLoginHandler; + _xmpp.OnSocketError -= ErrorHandler; + _xmpp.OnSaslStart -= SaslStartHandler; + _xmpp.Close(); + } + + /// + /// Create a HarmonyClient with pre-authenticated token + /// + /// IP or hostname + /// token which is created via an authentication via myharmony.com + /// Port to connect to, 5222 is the default + /// + public static HarmonyClient Create(string host, string token, int port = 5222) + { + return new HarmonyClient(host, token, port); + } + + /// + /// Create a harmony client via myharmony.com authenification + /// + /// IP or hostname + /// Port to connect to, default 5222 + /// HarmonyClient + public static async Task Create(string host, int port = 5222) + { + // Make a guest connection only to exchange the session token via the user authentication token + string sessionToken; + using (var client = new HarmonyClient(host, "guest", port)) + { + sessionToken = await client.CreateToken().ConfigureAwait(false); + } + + if (string.IsNullOrEmpty(sessionToken)) + { + throw new Exception("Could not get token from Harmony Hub."); + } + + // Create the client with the session token + return new HarmonyClient(host, sessionToken, port); + } + + /// + /// Send a document, ignore the response (but wait shortly for a possible error) + /// + /// Document + /// the time to wait for a possible error, if this is too small errors are ignored. + /// Task to await on + private async Task FireAndForgetAsync(Document document, int waitTimeout = 50) + { + // Check if the login was made, this blocks until there is a state + // And throws an exception if the login failed. + await _loginTaskCompletionSource.Task.ConfigureAwait(false); + + // Create the IQ to send + var iqToSend = GenerateIq(document); + + // Prepate the TaskCompletionSource, which is used to await the result + var resultTaskCompletionSource = new TaskCompletionSource(); + _resultTaskCompletionSources[iqToSend.Id] = resultTaskCompletionSource; + + Debug.WriteLine("Sending (ignoring response):"); + Debug.WriteLine(iqToSend.ToString()); + // Start the sending + _xmpp.Send(iqToSend); + + // Await, to make sure there wasn't an error + var task = await Task.WhenAny(resultTaskCompletionSource.Task, Task.Delay(waitTimeout)).ConfigureAwait(false); + + // Remove the result task, as we no longer need it. + _resultTaskCompletionSources.Remove(iqToSend.Id); + + // This makes sure the exception, if there was one, is unwrapped + await task; + } + + /// + /// Generate an IQ for the supplied Document + /// + /// Document + /// IQ + private static IQ GenerateIq(Document document) + { + // Create the IQ to send + var iqToSend = new IQ + { + Type = IqType.get, + Namespace = "", + From = "1", + To = "guest" + }; + + // Add the real content for the Harmony + iqToSend.AddChild(document); + + // Generate an unique ID, this is used to correlate the reply to the request + iqToSend.GenerateId(); + return iqToSend; + } + + /// + /// Get the data from the IQ response object + /// + /// IQ response object + /// string with the data of the element + private string GetData(IQ iq) + { + if (iq.HasTag("oa")) + { + var oaElement = iq.SelectSingleElement("oa"); + // Keep receiving messages until we get a 200 status + // Activity commands send 100 (continue) until they finish + var errorCode = oaElement.GetAttribute("errorcode"); + if ("200".Equals(errorCode)) + { + return oaElement.GetData(); + } + } + return null; + } + + /// + /// This event is triggered when the current activity is changed + /// + public event EventHandler OnActivityChanged; + + /// + /// This event is triggered when the connection closes + /// + public event EventHandler OnConnectionClosed; + + /// + /// Send a document, await the response and return it + /// + /// Document + /// Timeout for waiting on the response, if this passes a timeout exception is thrown + /// IQ response + private async Task RequestResponseAsync(Document document, int timeout = 2000) + { + // Check if the login was made, this blocks until there is a state + // And throws an exception if the login failed. + await _loginTaskCompletionSource.Task.ConfigureAwait(false); + + // Create the IQ to send + var iqToSend = GenerateIq(document); + + // Prepate the TaskCompletionSource, which is used to await the result + var resultTaskCompletionSource = new TaskCompletionSource(); + _resultTaskCompletionSources[iqToSend.Id] = resultTaskCompletionSource; + + Debug.WriteLine("Sending:"); + Debug.WriteLine(iqToSend.ToString()); + + // Create the action which is called when a timeout occurs + Action timeoutAction = () => + { + // Remove the registration, it is no longer needed + _resultTaskCompletionSources.Remove(iqToSend.Id); + // Pass the timeout exception to the await + resultTaskCompletionSource.TrySetException(new TimeoutException($"Timeout while waiting on response {iqToSend.Id} after {timeout}")); + }; + + // Start the sending + _xmpp.Send(iqToSend); + + // Setup the timeout handling + var cancellationTokenSource = new CancellationTokenSource(timeout); + using (cancellationTokenSource.Token.Register(timeoutAction)) + { + // Await / block until an reply arrives or the timeout happens + return await resultTaskCompletionSource.Task.ConfigureAwait(false); + } + } + + #region Authentication + + /// + /// Send message to HarmonyHub, wait for SessionToken + /// + /// session token + public async Task CreateToken() + { + var iq = await RequestResponseAsync(HarmonyDocuments.LogitechPairDocument()).ConfigureAwait(false); + var sessionData = GetData(iq); + if (sessionData != null) { + foreach (var pair in sessionData.Split(':')) { + if (pair.StartsWith("identity")) + { + return pair.Split('=')[1]; + } + } + } + throw new Exception("Wrong data"); + } + + #endregion + + #region Event Handlers + + /// + /// Handle incomming messages + /// + /// + /// + private void OnMessage(object sender, Message message) + { + if (!message.HasTag("event")) + { + return; + } + // Check for the activity changed data, see here: https://github.com/swissmanu/harmonyhubjs-client/blob/master/docs/protocol/startActivityFinished.md + var eventElement = message.SelectSingleElement("event"); + var eventData = eventElement.GetData(); + if (eventData == null) + { + return; + } + foreach (var pair in eventData.Split(':')) + { + if (!pair.StartsWith("activityId")) + { + continue; + } + var activityId = pair.Split('=')[1]; + OnActivityChanged?.Invoke(this, activityId); + } + } + + /// + /// Configure Sasl not to use auto and PLAIN for authentication + /// + /// object + /// SaslEventArgs + private void SaslStartHandler(object sender, SaslEventArgs saslEventArgs) + { + saslEventArgs.Auto = false; + saslEventArgs.Mechanism = "PLAIN"; + } + + /// + /// Handle login by completing the _loginTaskCompletionSource + /// + /// + private void OnLoginHandler(object sender) + { + _loginTaskCompletionSource.TrySetResult(true); + } + + /// + /// Lookup the TaskCompletionSource for the IQ message and try to set the result. + /// + /// object + /// IQ + private void OnIqResponseHandler(object sender, IQ iq) + { + Debug.WriteLine("Received event " + iq.Id); + Debug.WriteLine(iq.ToString()); + TaskCompletionSource resulTaskCompletionSource; + if ((iq.Id != null) && _resultTaskCompletionSources.TryGetValue(iq.Id, out resulTaskCompletionSource)) + { + // Error handling from XMPP + if (iq.Error != null) + { + var errorMessage = iq.Error.ErrorText; + Debug.WriteLine(errorMessage); + resulTaskCompletionSource.TrySetException(new Exception(errorMessage)); + // Result task is longer needed in the lookup + _resultTaskCompletionSources.Remove(iq.Id); + return; + } + + // Message processing (error handling) + if (iq.HasTag("oa")) + { + var oaElement = iq.SelectSingleElement("oa"); + + // Check error code + var errorCode = oaElement.GetAttribute("errorcode"); + // 100 -> continue + if ("100".Equals(errorCode)) + { + // Ignoring 100 continue + Debug.WriteLine("Ignoring, expecting more to come."); + + // TODO: Insert code to handle progress updates for the startActivity + } + // 200 -> OK + else if ("200".Equals(errorCode)) + { + resulTaskCompletionSource.TrySetResult(iq); + + // Result task is longer needed in the lookup + _resultTaskCompletionSources.Remove(iq.Id); + } + else + { + // We didn't get a 100 or 200, this must mean there was an error + var errorMessage = oaElement.GetAttribute("errorstring"); + Debug.WriteLine(errorMessage); + // Set the exception on the TaskCompletionSource, it will be picked up in the await + resulTaskCompletionSource.TrySetException(new Exception(errorMessage)); + + // Result task is longer needed in the lookup + _resultTaskCompletionSources.Remove(iq.Id); + } + } + else + { + Debug.WriteLine("Unexpected content"); + } + } + else + { + Debug.WriteLine("No matching result task found."); + } + } + + /// + /// Help with login errors + /// + /// object + /// Exception + private void ErrorHandler(object sender, Exception ex) + { + if (_loginTaskCompletionSource.Task.Status == TaskStatus.Created) + { + _loginTaskCompletionSource.TrySetException(ex); + } + else + { + Debug.WriteLine(ex.ToString()); + } + } + + #endregion + + #region Send Messages to HarmonyHub + + /// + /// Request the configuration from the hub + /// + /// HarmonyConfig + public async Task GetConfigAsync() + { + var iq = await RequestResponseAsync(HarmonyDocuments.ConfigDocument()).ConfigureAwait(false); + var config = GetData(iq); + if (config != null) + { + return Serializer.FromJson(config); + } + throw new Exception("No data found"); + } + + /// + /// Send message to HarmonyHub to start a given activity + /// Result is parsed by OnIq based on ClientCommandType + /// + /// string + public async Task StartActivityAsync(string activityId) + { + await RequestResponseAsync(HarmonyDocuments.StartActivityDocument(activityId)).ConfigureAwait(false); + } + + /// + /// Send message to HarmonyHub to request current activity + /// Result is parsed by OnIq based on ClientCommandType + /// + /// string with the current activity + public async Task GetCurrentActivityAsync() + { + var iq = await RequestResponseAsync(HarmonyDocuments.GetCurrentActivityDocument()).ConfigureAwait(false); + var currentActivityData = GetData(iq); + if (currentActivityData != null) + { + return currentActivityData.Split('=')[1]; + } + throw new Exception("No data found in IQ"); + } + + /// + /// Send message to HarmonyHub to request to press a button + /// Result is parsed by OnIq based on ClientCommandType + /// + /// string with the ID of the device + /// string with the command for the device + /// true for press, false for release + /// Timestamp for the command, e.g. send a press with 0 and a release with 100 + public async Task SendCommandAsync(string deviceId, string command, bool press = true, int? timestamp = null) + { + var document = HarmonyDocuments.IrCommandDocument(deviceId, command, press, timestamp); + await FireAndForgetAsync(document).ConfigureAwait(false); + } + + /// + /// Send a message that a button was pressed + /// Result is parsed by OnIq based on ClientCommandType + /// + /// string with the ID of the device + /// string with the command for the device + /// The time between the press and release, default 100ms + public async Task SendKeyPressAsync(string deviceId, string command, int timespan = 100) + { + var now = (int) DateTime.Now.ToUniversalTime().Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds; + var press = HarmonyDocuments.IrCommandDocument(deviceId, command, true, now - timespan); + await FireAndForgetAsync(press).ConfigureAwait(false); + var release = HarmonyDocuments.IrCommandDocument(deviceId, command, false, timespan); + await FireAndForgetAsync(release).ConfigureAwait(false); + } + + /// + /// Send message to HarmonyHub to request to turn off all devices + /// + public async Task TurnOffAsync() + { + var currentActivity = await GetCurrentActivityAsync().ConfigureAwait(false); + if (currentActivity != "-1") + { + await StartActivityAsync("-1").ConfigureAwait(false); + } + } + + #endregion + } } \ No newline at end of file