mirror of
https://bitbucket.org/myhomie/mycorerepository.git
synced 2025-12-06 01:31:19 +00:00
634 lines
34 KiB
C#
634 lines
34 KiB
C#
using Mqtt.Client.AspNetCore.Services;
|
|
using MyCore.Interfaces.DTO;
|
|
using MyCore.Interfaces.Models;
|
|
using MyCore.Interfaces.Models.Providers.Zigbee.Zigbee2Mqtt;
|
|
using MyCore.Service.Services;
|
|
using MyCore.Services.MyControlPanel;
|
|
using Newtonsoft.Json;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Threading.Tasks;
|
|
using static Mqtt.Client.AspNetCore.Services.MqttClientMerossService;
|
|
|
|
namespace MyCore.Services.Devices
|
|
{
|
|
public class ActionService
|
|
{
|
|
private static Provider currentProvider;
|
|
private static Device deviceTrigger;
|
|
private static string providerFromTopic;
|
|
private static string deviceServiceName;
|
|
public static async Task HandleActionFromMQTTAsync(string topic, string message, DeviceDatabaseService _DeviceDatabaseService, GroupDatabaseService _GroupDatabaseService, ProviderDatabaseService _ProviderDatabaseService, RoomDatabaseService _RoomDatabaseService, AutomationDatabaseService _AutomationDatabaseService, string homeId)
|
|
{
|
|
System.Console.WriteLine($"Received message {message}");
|
|
|
|
string[] topicSplit = topic.Split('/');
|
|
try {
|
|
providerFromTopic = topicSplit[0];
|
|
deviceServiceName = topicSplit[1];
|
|
} catch (Exception ex) {
|
|
System.Console.WriteLine($"Exeption throw when fetching provider and device from topic {topic} - Error: {ex}");
|
|
}
|
|
|
|
switch (providerFromTopic) {
|
|
case "zigbee2mqtt":
|
|
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
currentProvider = Enum.IsDefined(typeof(ProviderType), providerFromTopic) ? _ProviderDatabaseService.GetByType((ProviderType) Enum.Parse(typeof(ProviderType), providerFromTopic.ToLower())) : null;
|
|
|
|
if (currentProvider != null)
|
|
{
|
|
var automations = _AutomationDatabaseService.GetActiveByProvider(currentProvider.Id);
|
|
|
|
deviceTrigger = _DeviceDatabaseService.GetByName(deviceServiceName).FirstOrDefault();
|
|
|
|
if (deviceTrigger != null)
|
|
{
|
|
try
|
|
{
|
|
// if is not a set => check automation
|
|
if (topicSplit.Length <= 2)
|
|
{
|
|
foreach (var automation in automations)
|
|
{
|
|
try {
|
|
// todo check if not null and if more than one element
|
|
if (deviceTrigger != null && automation.Triggers.Any(t => t.DeviceId == deviceTrigger.Id))
|
|
{
|
|
var automationTriggers = automation.Triggers.Where(t => t.DeviceId == deviceTrigger.Id).ToList();
|
|
|
|
// Test for each automation trigger found
|
|
foreach (var automationTrigger in automationTriggers)
|
|
{
|
|
List<Expose> exposes = GetDeviceExposes(deviceTrigger);
|
|
|
|
var triggerStateValueCheck = (object)null;
|
|
|
|
// Try to get automationTrigger.StateName of zigbee device
|
|
var expose = exposes.Where(e => e.name == automationTrigger.StateName).FirstOrDefault();
|
|
bool validTrigger = expose != null; // No expose found.. => the device has not the correct state => not correct automation
|
|
|
|
try
|
|
{
|
|
// Try to get specific field from message
|
|
var dic = JsonConvert.DeserializeObject<Dictionary<string, string>>(message);
|
|
triggerStateValueCheck = dic[automationTrigger.StateName]; // if action slide => get slide
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
validTrigger = false;
|
|
}
|
|
|
|
|
|
if (validTrigger && automationTrigger.StateValue.ToLower() == triggerStateValueCheck.ToString().ToLower())
|
|
{
|
|
// Correct trigger !
|
|
System.Console.WriteLine($"Correct trigger for automation {automation.Name}");
|
|
|
|
var isConditionsRespected = CheckConditions(automation.Conditions);
|
|
|
|
if (!isConditionsRespected.Any(cr => !cr))
|
|
{
|
|
System.Console.WriteLine("Conditions respected !");
|
|
foreach (var action in automation.Actions)
|
|
{
|
|
|
|
var DeviceNameForAction = "";
|
|
Device actionDeviceToTest = new Device();
|
|
|
|
// Retrieve action type
|
|
switch (action.Type)
|
|
{
|
|
case ActionType.DEVICE:
|
|
var deviceAction = _DeviceDatabaseService.GetById(action.DeviceId);
|
|
var providerActionTest = _ProviderDatabaseService.GetById(homeId, action.ProviderId);
|
|
|
|
DeviceNameForAction = deviceAction.Name;
|
|
actionDeviceToTest = deviceAction;
|
|
System.Console.WriteLine($"We get a device action ! Name={deviceAction.Name} Type={deviceAction.Type}");
|
|
System.Console.WriteLine($"Check action provider type ! Type={providerActionTest.Type} Name={providerActionTest.Name}");
|
|
break;
|
|
case ActionType.GROUP:
|
|
var groupAction = _GroupDatabaseService.GetById(action.GroupId);
|
|
DeviceNameForAction = groupAction.Name;
|
|
|
|
System.Console.WriteLine($"We get a group action ! Name={groupAction.Name} Type={groupAction.Type}");
|
|
System.Console.WriteLine($"Check action zigbeeGroupAction type ! Type={groupAction.Type}");
|
|
|
|
// Check state of first device of a group
|
|
actionDeviceToTest = _DeviceDatabaseService.GetByGroup(groupAction.Id).FirstOrDefault(); // TODO : Send error if no device found !
|
|
break;
|
|
case ActionType.MQTT:
|
|
// take raw request and send it !
|
|
RawRequestMQTT rawRequestMQTT = JsonConvert.DeserializeObject<RawRequestMQTT>(action.RawRequest);
|
|
|
|
if (rawRequestMQTT != null)
|
|
{
|
|
// SEND REQUEST
|
|
System.Console.WriteLine($"Send raw request mqtt! topic:{rawRequestMQTT.topic} - message: {rawRequestMQTT.message}");
|
|
MqttClientService.PublishMessage(rawRequestMQTT.topic, rawRequestMQTT.message);
|
|
}
|
|
break;
|
|
case ActionType.HTTP: // Correct way ?
|
|
// TODO
|
|
break;
|
|
}
|
|
|
|
var providerAction = _ProviderDatabaseService.GetById(homeId, action.ProviderId);
|
|
|
|
// Check if device exist
|
|
if (actionDeviceToTest != null && providerAction != null)
|
|
{
|
|
switch (providerAction.Type)
|
|
{
|
|
case ProviderType.zigbee2mqtt:
|
|
try
|
|
{
|
|
ActionOnZigbee2Mqtt(actionDeviceToTest, action, DeviceNameForAction);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Console.WriteLine($"ActionOnZigbee2Mqtt result in error: {ex}");
|
|
}
|
|
break;
|
|
case ProviderType.meross:
|
|
try
|
|
{
|
|
ActionOnMeross(_DeviceDatabaseService, actionDeviceToTest, action, DeviceNameForAction);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Console.WriteLine($"ActionOnMeross result in error: {ex}");
|
|
}
|
|
break;
|
|
case ProviderType.yeelight:
|
|
try
|
|
{
|
|
ActionOnYeelight(_DeviceDatabaseService, actionDeviceToTest, action);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Console.WriteLine($"ActionOnYeelight result in error: {ex}");
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
System.Console.WriteLine($"Device or group found in action incorrect");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
System.Console.WriteLine($"One or more condition aren't respected");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (Exception ex) { System.Console.WriteLine($"Exeption in one of automation logic - {ex}"); }
|
|
}
|
|
// Update last state of devices
|
|
deviceTrigger.LastStateDate = DateTime.Now;
|
|
deviceTrigger.LastState = message;
|
|
_DeviceDatabaseService.Update(deviceTrigger);
|
|
}
|
|
else
|
|
{
|
|
// do nothing is a set
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Console.WriteLine($"Exeption from automation logic - {ex}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
System.Console.WriteLine($"Current device Trigger not found - {deviceServiceName}");
|
|
}
|
|
}
|
|
else {
|
|
System.Console.WriteLine($"Current provider not found - {providerFromTopic}");
|
|
}
|
|
}
|
|
|
|
private async static void ActionOnYeelight(DeviceDatabaseService deviceDatabaseService, Device actionDeviceToTest, Interfaces.Models.Action action)
|
|
{
|
|
if (YeelightService.devices.Count <= 0)
|
|
{
|
|
await YeelightService.GetDevices();
|
|
}
|
|
var lamp = YeelightService.devices.Where(d => d.Hostname == actionDeviceToTest.IpAddress).FirstOrDefault();
|
|
if (lamp != null)
|
|
{
|
|
System.Console.WriteLine($"Yeelight type !");
|
|
|
|
// Get device last state
|
|
var dic = new List<AutomationState>();
|
|
if (actionDeviceToTest.LastState != null)
|
|
{
|
|
dic = JsonConvert.DeserializeObject<List<AutomationState>>(actionDeviceToTest.LastState);
|
|
}
|
|
|
|
try
|
|
{
|
|
switch (actionDeviceToTest.Type)
|
|
{
|
|
case DeviceType.Light:
|
|
var states = action.States.Where(s => s.Name == "state" || s.Name == "brightness").ToList();
|
|
|
|
if (states.Count >= 0)
|
|
{
|
|
if (dic.Count > 0)
|
|
{
|
|
/*if (dic["state"].ToString().ToLower() == state.Value.ToLower() && action.States.Count <= 1) // workaround if brightness not the same
|
|
{
|
|
throw new Exception($"Action device is already at the good state : {state.Name} {state.Value}");
|
|
}*/
|
|
}
|
|
|
|
foreach (var state in states)
|
|
{
|
|
switch (state.Name)
|
|
{
|
|
case "state":
|
|
switch (state.Value.ToLower())
|
|
{
|
|
case "on":
|
|
Task.Run(async () => { await YeelightService.TurnOn(lamp, null); });
|
|
break;
|
|
case "off":
|
|
Task.Run(async () => { await YeelightService.TurnOff(lamp); });
|
|
break;
|
|
case "toggle":
|
|
Task.Run(async () => { await YeelightService.Toggle(lamp); });
|
|
break;
|
|
}
|
|
break;
|
|
case "brightness":
|
|
var brightness = int.Parse(state.Value);
|
|
Task.Run(async () => { await YeelightService.TurnOn(lamp, brightness); });
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Update Last state
|
|
actionDeviceToTest.LastState = JsonConvert.SerializeObject(states);
|
|
actionDeviceToTest.LastStateDate = DateTime.Now;
|
|
deviceDatabaseService.Update(actionDeviceToTest);
|
|
}
|
|
else
|
|
{
|
|
throw new Exception($"Action device light in yeelight does not have correct expose");
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Console.WriteLine($"Not a valid action !");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
System.Console.WriteLine($"Yeelight - Lamp offline! - {actionDeviceToTest.Name}");
|
|
}
|
|
}
|
|
|
|
private static void ActionOnMeross(DeviceDatabaseService _DeviceDatabaseService, Device actionDeviceToTest, Interfaces.Models.Action action, string deviceNameForAction)
|
|
{
|
|
var merossDevice = _DeviceDatabaseService.GetById(actionDeviceToTest.Id);
|
|
|
|
if (merossDevice != null)
|
|
{
|
|
//As multisocket channel 0 is all the sockets, skip 0
|
|
|
|
// two state = first state = ON/OFF, second var = switch channels number (LIST) // TODO better check
|
|
if (action.States.Count >= 2 && action.States.Any(s => s.Name.ToLower() == "state" && (s.Value.ToLower() == "on" || s.Value.ToLower() == "off" || s.Value.ToLower() == "toggle")))
|
|
{
|
|
var state = action.States.Where(s => s.Name.ToLower() == "state").FirstOrDefault();
|
|
var channels = action.States.Where(s => s.Name.ToLower() == "channels").FirstOrDefault();
|
|
var plugIds = JsonConvert.DeserializeObject<List<int>>(channels.Value);
|
|
|
|
List<ToggleStatus> lastStatePlug = new List<ToggleStatus>(); // Off by default
|
|
// => TODO GET ALL POSSIBILITIES FROM DEVICE (number of channels)
|
|
|
|
if (actionDeviceToTest.LastState != null) // Get last state of device
|
|
{
|
|
try
|
|
{
|
|
lastStatePlug = JsonConvert.DeserializeObject<List<ToggleStatus>>(actionDeviceToTest.LastState);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Console.WriteLine($"Meross error: Last state ! - {ex}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// By default set to OFF
|
|
foreach (var plug in plugIds)
|
|
{
|
|
lastStatePlug.Add(ToggleStatus.OFF);
|
|
}
|
|
}
|
|
|
|
List<ToggleStatus> statuses = new List<ToggleStatus>();
|
|
var p = 0;
|
|
foreach (var plugId in plugIds)
|
|
{
|
|
ToggleStatus status = new ToggleStatus();
|
|
// check if device, check if multiprise
|
|
switch (merossDevice.Type)
|
|
{
|
|
case DeviceType.Multiplug:
|
|
case DeviceType.Plug:
|
|
if (state.Value.ToLower() == "toggle")
|
|
{
|
|
status = lastStatePlug[p] == ToggleStatus.OFF ? ToggleStatus.ON : ToggleStatus.OFF;
|
|
}
|
|
else
|
|
{
|
|
status = state.Value.ToLower() == "on" ? ToggleStatus.ON : ToggleStatus.OFF;
|
|
}
|
|
break;
|
|
}
|
|
// Send request
|
|
MqttClientMerossService.ExecuteCommand(actionDeviceToTest.ServiceIdentification, Method.SET, CommandMqtt.TOGGLEX, "", "", status, plugId);
|
|
statuses.Add(status);
|
|
p++;
|
|
}
|
|
|
|
// Update Last state
|
|
actionDeviceToTest.LastState = JsonConvert.SerializeObject(statuses);
|
|
actionDeviceToTest.LastStateDate = DateTime.Now;
|
|
_DeviceDatabaseService.Update(actionDeviceToTest);
|
|
}
|
|
else
|
|
{
|
|
throw new Exception("Meross: Request action invalid");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void ActionOnZigbee2Mqtt(Device device, Interfaces.Models.Action action, string deviceNameForAction)
|
|
{
|
|
var request = "";
|
|
var buildRequest = new Dictionary<string, object>();
|
|
|
|
System.Console.WriteLine($"zigbee2mqtt type !");
|
|
|
|
var actionDeviceExpose = GetDeviceExposes(device);
|
|
|
|
// Get device last state
|
|
var dic = new Dictionary<string, object>();
|
|
if (device.LastState != null)
|
|
{
|
|
dic = JsonConvert.DeserializeObject<Dictionary<string, object>>(device.LastState);
|
|
}
|
|
|
|
try
|
|
{
|
|
|
|
switch (device.Type)
|
|
{
|
|
case DeviceType.Light:
|
|
case DeviceType.Switch:
|
|
var actionDeviceExposeForFeatures = actionDeviceExpose.Where(e => e.type == ((DeviceType)device.Type).ToString().ToLower()).FirstOrDefault();
|
|
if (actionDeviceExposeForFeatures != null)
|
|
{
|
|
foreach (var state in action.States)
|
|
{
|
|
System.Console.WriteLine($"Check Action in light|switch ! {state.Name} {state.Value}");
|
|
if (actionDeviceExposeForFeatures.features.Any(ade => ade.name == state.Name))
|
|
{
|
|
// Comment this for test (ensure the request will be sent)
|
|
/*if (dic.Count > 0)
|
|
{
|
|
//if (device.Type == DeviceType.Light) {}
|
|
var test = dic["state"].ToString().ToLower();
|
|
var test0 = state.Value.ToLower();
|
|
if (dic["state"].ToString().ToLower() == state.Value.ToLower() && action.States.Count <= 1) // workaround if brightness not the same => For switch or light without brightness
|
|
{
|
|
throw new Exception($"Action device is already at the good state : {state.Name} {state.Value}");
|
|
}
|
|
}*/
|
|
|
|
if (state.Name == "brightness")
|
|
{
|
|
try
|
|
{
|
|
int brightness = int.Parse(state.Value);
|
|
// Add to request
|
|
buildRequest.Add(state.Name, brightness);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Console.WriteLine($"zigbee - Parse to int error {ex}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Add to request
|
|
buildRequest.Add(state.Name, state.Value);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new Exception($"Action device light|switch does not have expose of type {state.Name}");
|
|
}
|
|
}
|
|
|
|
// If any action ask for state update, check if device is already at the asked state
|
|
if (action.States.Any(s => s.Name == "state") && !action.IsForce)
|
|
{
|
|
bool sendRequest = true;
|
|
|
|
// If action doesn't ask for brightness just test if device is already at the asked state
|
|
if (action.States.Any(s => s.Name == "brightness"))
|
|
{
|
|
// Check state and brightness
|
|
// if we got state value (current value)
|
|
if (dic.Any(d => d.Key == "state"))
|
|
{
|
|
var currentValue = dic["state"].ToString().ToLower();
|
|
var askedValue = action.States.FirstOrDefault(s => s.Name == "state").Value.ToLower();
|
|
if (currentValue == askedValue)
|
|
{
|
|
// Check brightness difference
|
|
if (dic.Any(d => d.Key == "brightness"))
|
|
{
|
|
try
|
|
{
|
|
var currentValueBrightness = int.Parse(dic["brightness"].ToString().ToLower());
|
|
var askedValueBrightness = int.Parse(action.States.FirstOrDefault(s => s.Name == "brightness").Value.ToLower());
|
|
if (Math.Abs(currentValueBrightness - askedValueBrightness) <= 5) // brightness diff is lower than 5 => don't change brightness
|
|
sendRequest = false;
|
|
else
|
|
sendRequest = true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
sendRequest = false;
|
|
System.Console.WriteLine($"int parse error in brightness check", ex.Message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Check only state value
|
|
// if we got state value (current value)
|
|
if (dic.Any(d => d.Key == "state"))
|
|
{
|
|
var currentValue = dic["state"].ToString().ToLower();
|
|
var askedValue = action.States.FirstOrDefault(s => s.Name == "state").Value.ToLower();
|
|
if (currentValue == askedValue)
|
|
sendRequest = false;
|
|
}
|
|
}
|
|
|
|
if (!sendRequest)
|
|
throw new Exception($"Action device is already at the good state");
|
|
}
|
|
}
|
|
else
|
|
throw new Exception("Action device does not have expose of type light|switch");
|
|
break;
|
|
default:
|
|
foreach (var state in action.States)
|
|
{
|
|
System.Console.WriteLine($"Check Action ! {state.Name} {state.Value}");
|
|
if (!actionDeviceExpose.Any(ade => ade.name == state.Name))
|
|
{
|
|
throw new Exception($"Action device does not have expose of type {state.Name}");
|
|
}
|
|
|
|
}
|
|
break;
|
|
}
|
|
|
|
request = JsonConvert.SerializeObject(buildRequest);
|
|
|
|
// SEND REQUEST
|
|
//var requestToSend = $"Send request ! zigbee2mqtt/{deviceNameForAction}/set/{request}";
|
|
System.Console.WriteLine($"Send request ! zigbee2mqtt/{deviceNameForAction}/set/{request}");
|
|
|
|
MqttClientService.PublishMessage("zigbee2mqtt/" + deviceNameForAction + "/set", request);
|
|
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Console.WriteLine($"Not a valid action !");
|
|
}
|
|
}
|
|
|
|
private static List<bool> CheckConditions(List<Condition> conditions)
|
|
{
|
|
var isConditionsRespected = conditions.Count <= 0 ? new List<bool>() { true } : new List<bool>(new bool[conditions.Count]);
|
|
var i = 0;
|
|
foreach (var condition in conditions)
|
|
{
|
|
switch (condition.Type)
|
|
{
|
|
case ConditionType.STATE:
|
|
break;
|
|
case ConditionType.TIME:
|
|
TimeSpan now = DateTime.Now.TimeOfDay;
|
|
|
|
var test = JsonConvert.SerializeObject(now);
|
|
|
|
TimeSpan start = JsonConvert.DeserializeObject<TimeSpan>(condition.StartTime);
|
|
TimeSpan end = JsonConvert.DeserializeObject<TimeSpan>(condition.EndTime);
|
|
|
|
// => Test if ok with night !
|
|
|
|
if (start <= end)
|
|
{
|
|
// start and stop times are in the same day
|
|
if (now >= start && now <= end)
|
|
{
|
|
isConditionsRespected[i] = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// start and stop times are in different days
|
|
if (now >= start || now <= end)
|
|
{
|
|
isConditionsRespected[i] = true;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
i++;
|
|
}
|
|
|
|
return isConditionsRespected;
|
|
}
|
|
|
|
private static List<Expose> GetDeviceExposes(Device device)
|
|
{
|
|
List<Expose> exposes = new List<Expose>();
|
|
// Get Exposes for the zigbee device
|
|
foreach (var supportedOperation in device.SupportedOperations)
|
|
{
|
|
exposes.Add(JsonConvert.DeserializeObject<Expose>(supportedOperation));
|
|
}
|
|
return exposes;
|
|
}
|
|
|
|
private static System.Type GetStateType(Expose expose)
|
|
{
|
|
switch (expose.type)
|
|
{
|
|
// e.g.: occupancy
|
|
case "binary":
|
|
if (expose.value_toggle == null)
|
|
{
|
|
// value are boolean
|
|
return true.GetType();
|
|
}
|
|
else
|
|
{
|
|
// value are string
|
|
return "".GetType();
|
|
}
|
|
case "numeric":
|
|
return new long().GetType();
|
|
case "enum":
|
|
return expose.values.GetType();
|
|
break;
|
|
/*case "text":
|
|
break;
|
|
case "light":
|
|
break;
|
|
case "composite":
|
|
break;
|
|
case "switch":
|
|
break;*/
|
|
/*case "fan":
|
|
break;
|
|
case "cover":
|
|
break;
|
|
case "lock":
|
|
break;
|
|
case "climate":
|
|
break;*/
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
}
|