mirror of
https://bitbucket.org/myhomie/mycorerepository.git
synced 2025-12-06 01:31:19 +00:00
529 lines
18 KiB
C#
529 lines
18 KiB
C#
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 static string _devList = $"{_merossUrl}/v1/Device/devList";
|
|
|
|
private static string username = "thomas.fransolet@hotmail.be";
|
|
private static string password = "Coconuts07";
|
|
|
|
private static ResultToken resultToken;
|
|
|
|
private static List<MerossDevice> 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<ResultToken>(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);
|
|
|
|
if (message != null) {
|
|
PublishMessage(topic, message);
|
|
}
|
|
}
|
|
|
|
private static string BuildMQTTMessage(Method method, string namespaceVar, string payloadVar)
|
|
{
|
|
HeaderMqtt headerMqtt = new HeaderMqtt();
|
|
|
|
if (resultToken != null)
|
|
{
|
|
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;
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
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<string> 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<MerossDevice> 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<List<MerossDevice>>(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
|
|
}
|
|
|
|
}
|