using MQTTnet; using MQTTnet.Client; using MQTTnet.Client.Connecting; using MQTTnet.Client.Disconnecting; using MQTTnet.Client.Options; using MQTTnet.Formatter; using MyCore.Interfaces.Models; using MyCore.Services.Devices; using MyCore.Services.MyControlPanel; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Security.Authentication; using System.Text; using System.Threading; using System.Threading.Tasks; using static MyCore.Interfaces.Models.DeviceAbilities; namespace Mqtt.Client.AspNetCore.Services { public class MqttClientMerossService : IMqttMerossClientService { // API HTTP private const string _merossUrl = "https://iot.meross.com"; private static string _secret = "23x17ahWarFH6w29"; private string _loginUrl = $"{_merossUrl}/v1/Auth/Login"; private string _logUrl = $"{_merossUrl}/v1/log/user"; private string _devList = $"{_merossUrl}/v1/Device/devList"; private static string username = "thomas.fransolet@hotmail.be"; private static string password = "Coconuts07"; private static ResultToken resultToken; private List allDevices; // API MQTT private const int port = 2001; private const string domain = "iot.meross.com"; private static IMqttClient client; private IMqttClientOptions options; private string _userMQTT; private string _passwordMQTT; public string clientId = ""; public static string appId = ""; public class Credential { public string email; public string password; } public class ResultToken { public string userid; public string email; public string token; public string key; } public enum RequestType { Login, Log, DeviceList } public MqttClientMerossService(IMqttClientOptions _options) { try { // TODO /*this.username = username; this.password = password;*/ // LOGIN var loginTask = Task.Run(() => PostURI(new Uri(_loginUrl), RequestType.Login)); loginTask.Wait(); if (loginTask.Result != "") { //RESULT TOKEN var data = ((JObject)JsonConvert.DeserializeObject(loginTask.Result))["data"]; resultToken = JsonConvert.DeserializeObject(data.ToString()); allDevices = GetMerossDevices(); // RETRIEVE LOG TODO Create a GetLogs method /*var logTask = Task.Run(() => PostURI(new Uri(_logUrl), RequestType.Log)); logTask.Wait(); if (logTask.Result != "") { data = ((JObject)JsonConvert.DeserializeObject(logTask.Result))["data"]; }*/ } } catch (Exception e) { resultToken = null; throw new AuthenticationException("Bad service username or password"); } string stringForMD5 = resultToken.userid + resultToken.key; string hashed_password = CreateMD5(stringForMD5).ToLower(); // Check if we can hardcode it -> seems ok 20/01/2020 var uuid = "01df8092-d790-4377-8481-5b73aef045c6"; GenerateClientAndAppId(uuid); options = new MqttClientOptionsBuilder() .WithClientId(clientId) .WithTcpServer(domain, port) .WithCredentials(resultToken.userid, hashed_password) .WithCleanSession() .WithTls() .WithProtocolVersion(MqttProtocolVersion.V311) .Build(); client = new MqttFactory().CreateMqttClient(); ConfigureMqttClient(); } private void ConfigureMqttClient() { client.ConnectedHandler = this; client.DisconnectedHandler = this; client.ApplicationMessageReceivedHandler = this; } public Task HandleApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs e) { Console.WriteLine("### RECEIVED APPLICATION MESSAGE ###"); Console.WriteLine($"+ Topic = {e.ApplicationMessage.Topic}"); var payload = ""; if (e.ApplicationMessage.Payload != null) { Console.WriteLine($"+ Payload = {Encoding.UTF8.GetString(e.ApplicationMessage.Payload)}"); payload = Encoding.UTF8.GetString(e.ApplicationMessage.Payload); } Console.WriteLine($"+ QoS = {e.ApplicationMessage.QualityOfServiceLevel}"); Console.WriteLine($"+ Retain = {e.ApplicationMessage.Retain}"); Console.WriteLine(); var topic = e.ApplicationMessage.Topic; /* Example : * * * Toggle channel 0 multiprise * topic = /appliance/1909206725106690802148e1e95243fd/subscribe * * payload = * {"header": {"from": "/app/174744-2c2c67eab37f04625d60dc678e5a2349/subscribe", * "messageId": "5e2ac8700552ef089da05f8543993319", * "method": "SET", * "namespace": "Appliance.Control.ToggleX", * "payloadVersion": 1, * "sign": "18469fb032695f3144f060e65f3fad26", * "timestamp": 1579540738}, * "payload": * {"togglex": * {"onoff": 1, "channel": 0} * } * } */ return null; } public async Task HandleConnectedAsync(MqttClientConnectedEventArgs eventArgs) { System.Console.WriteLine("connected meross"); var topic = $"/app/{resultToken.userid}-{appId.ToLower()}/subscribe"; await client.SubscribeAsync(topic); //await client.SubscribeAsync("#"); //await client.SubscribeAsync(new TopicFilterBuilder().WithTopic(topic).Build()); /*MerossDevice multisocket = allDevices.Where(d => d.onlineStatus == 1 && d.deviceType == "mss425f").FirstOrDefault(); ExecuteCommand(multisocket.uuid, Method.GET, CommandMqtt.ABILITY); foreach (var device in allDevices.Where(d => d.deviceType == "mss310" && d.onlineStatus == 1).ToList()) { ExecuteCommand(device.uuid, Method.GET, CommandMqtt.ABILITY); ExecuteCommand(device.uuid, Method.GET, CommandMqtt.ELECTRICITY); } Console.WriteLine("### SUBSCRIBED ###");*/ } public static void ExecuteCommand(string uuid, Method method, CommandMqtt command, string payload = null, string callback = null, ToggleStatus toggleStatus = 0, int channelChosen = 0) { var topic = $"/appliance/{uuid}/subscribe"; var namespaceVar = ""; var payloadVar = payload; bool multisocket = false; // TODO Check device type.. and add switch case switch (command) { case CommandMqtt.ABILITY: namespaceVar = Common.ABILITY; break; case CommandMqtt.CONSUMPTIONX: namespaceVar = PlugsAndBulbs.TOGGLEX; break; case CommandMqtt.TOGGLE: namespaceVar = PlugsAndBulbs.TOGGLE; break; case CommandMqtt.TOGGLEX: multisocket = true; namespaceVar = PlugsAndBulbs.TOGGLEX; break; case CommandMqtt.ELECTRICITY: namespaceVar = PlugsAndBulbs.ELECTRICITY; break; } // if a plug // and if a toggleX ability if (method == Method.SET && multisocket) { PayLoadToggleX payLoadToggleX = new PayLoadToggleX(); ToggleX toggleX = new ToggleX(); toggleX.onoff = toggleStatus.GetHashCode(); toggleX.channel = channelChosen; payLoadToggleX.togglex = toggleX; payloadVar = JsonConvert.SerializeObject(payLoadToggleX); } // if a plug // and if hasn't toggleX ability if (method == Method.SET && !multisocket) { PayLoadToggle payLoadToggle = new PayLoadToggle(); Toggle toggle = new Toggle(); toggle.onoff = toggleStatus.GetHashCode(); payLoadToggle.toggle = toggle; payloadVar = JsonConvert.SerializeObject(payLoadToggle); } var message = BuildMQTTMessage(method, namespaceVar, payloadVar); PublishMessage(topic, message); } private static string BuildMQTTMessage(Method method, string namespaceVar, string payloadVar) { HeaderMqtt headerMqtt = new HeaderMqtt(); headerMqtt.from = $"/app/{resultToken.userid}-{appId.ToLower()}/subscribe"; headerMqtt.messageId = CreateMD5(GenerateNonce(16).ToUpper()).ToLower(); headerMqtt.method = method.ToString(); headerMqtt.Namespace = namespaceVar; headerMqtt.payloadVersion = 1; headerMqtt.timestamp = new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds(); headerMqtt.sign = CreateMD5($"{headerMqtt.messageId}{resultToken.key}{(headerMqtt.timestamp).ToString()}").ToLower(); RequestMQTT requestMQTT = new RequestMQTT(); requestMQTT.header = headerMqtt; if (payloadVar != null) requestMQTT.payload = JsonConvert.DeserializeObject(payloadVar); else requestMQTT.payload = new Object(); var data = JsonConvert.SerializeObject(requestMQTT); // Change Namespace to namespace data = data.Replace("Namespace", "namespace"); return data; } public async Task HandleDisconnectedAsync(MqttClientDisconnectedEventArgs eventArgs) { if (!client.IsConnected) { await client.ReconnectAsync(); } } public async Task StartAsync(CancellationToken cancellationToken) { await client.ConnectAsync(options); if (!client.IsConnected) { await client.ReconnectAsync(); } } public async Task StopAsync(CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { var disconnectOption = new MqttClientDisconnectOptions { ReasonCode = MqttClientDisconnectReason.NormalDisconnection, ReasonString = "NormalDiconnection" }; await client.DisconnectAsync(disconnectOption, cancellationToken); } await client.DisconnectAsync(); } public static async Task PublishMessage(string topic, string message) { var mqttMessage = new MqttApplicationMessageBuilder() .WithTopic(topic) .WithPayload(message) .WithExactlyOnceQoS() .WithRetainFlag() .Build(); if (client.IsConnected) await client.PublishAsync(mqttMessage).ContinueWith(res => { if (res.Status == TaskStatus.RanToCompletion) { } }); } public static string CreateMD5(string input) { // Use input string to calculate MD5 hash using (System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create()) { byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(input); byte[] hashBytes = md5.ComputeHash(inputBytes); // Convert the byte array to hexadecimal string StringBuilder sb = new StringBuilder(); for (int i = 0; i < hashBytes.Length; i++) { sb.Append(hashBytes[i].ToString("X2")); } return sb.ToString(); } } public static async Task PostURI(Uri u, RequestType requestType) { var response = string.Empty; var requestParam = ""; string nonce = GenerateNonce(16).ToUpper(); var timeStamp = new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds(); switch (requestType) { case RequestType.Login: requestParam = Base64Encode(JsonConvert.SerializeObject(new Credential { email = username, password = password }).ToString()); break; case RequestType.Log: requestParam = Base64Encode("{'extra': { }, 'model': 'Android,Android SDK built for x86_64', 'system': 'Android','uuid': '493dd9174941ed58waitForOpenWifi', 'vendor': 'Meross', 'version': '6.0'}"); break; case RequestType.DeviceList: requestParam = "e30="; break; } string stringForMD5 = stringForMD5 = _secret + timeStamp.ToString() + nonce + requestParam; string md5 = CreateMD5(stringForMD5); var payloadLogin = "{ \"params\":\"" + requestParam + "\",\"sign\":\"" + md5.ToLower() + "\",\"timestamp\":" + timeStamp + ",\"nonce\":\"" + nonce + "\"}"; using (var client = new HttpClient()) { HttpContent c = new StringContent(payloadLogin, Encoding.UTF8, "application/json"); if (resultToken != null) { client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", resultToken.token); } else { client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic"); } client.DefaultRequestHeaders.Add("vender", "Meross"); client.DefaultRequestHeaders.Add("AppVersion", "1.3.0"); client.DefaultRequestHeaders.Add("AppLanguage", "EN"); client.DefaultRequestHeaders.Add("User-Agent", "okhttp/3.6.0"); HttpResponseMessage result = await client.PostAsync(u, c); if (result.IsSuccessStatusCode) { response = await result.Content.ReadAsStringAsync(); } } return response; } private void GenerateClientAndAppId(string uuid) { var md5Result = CreateMD5($"API{uuid}"); appId = md5Result; clientId = $"app:{md5Result}"; } private static string validChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; private static Random random = new Random(); private static string GenerateNonce(int length) { var nonceString = new StringBuilder(); for (int i = 0; i < length; i++) { nonceString.Append(validChars[random.Next(0, validChars.Length - 1)]); } return nonceString.ToString(); } public static string Base64Encode(string plainText) { var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText); return System.Convert.ToBase64String(plainTextBytes); } public static List GetMerossDevices() { // GET DEVICE LIST var deviceTask = Task.Run(() => PostURI(new Uri(_devList), RequestType.DeviceList)); deviceTask.Wait(); if (deviceTask.Result != "") { var data = ((JObject)JsonConvert.DeserializeObject(deviceTask.Result))["data"]; // RETRIEVE ALL DEVICES allDevices = JsonConvert.DeserializeObject>(data.ToString()); } else throw new HttpRequestException("Error retrieving meross devices"); return allDevices; } #region MQTT public class RequestMQTT { public HeaderMqtt header; public Object payload; } public class HeaderMqtt { public string from; public string messageId; public string method; public string Namespace; public int payloadVersion; public string sign; public long timestamp; } public class PayLoadToggle { // as it's not a multi-socket public int channel = 0; public Toggle toggle; } public class Toggle { // 1 = on or 0 = off public int onoff; } public class PayLoadToggleX { public ToggleX togglex; } public class ToggleX { // 1 = on or 0 = off public int onoff; // number of the socket of the multi-socket public int channel; } // TODO Create ability class related to device public enum CommandMqtt { ABILITY, CONSUMPTIONX, ELECTRICITY, TOGGLE, TOGGLEX } public enum Method { GET, SET } public enum ToggleStatus { OFF, ON } #endregion MQTT } }