657 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, LocationDatabaseService _LocationDatabaseService, AutomationDatabaseService _AutomationDatabaseService, string userId)
{
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":
UpdateZigbee2MqttConfigOrStateAsync(topic, message, userId, _DeviceDatabaseService, _GroupDatabaseService, _ProviderDatabaseService, _LocationDatabaseService);
break;
default:
break;
}
currentProvider = _ProviderDatabaseService.GetByType(providerFromTopic);
if (currentProvider != null)
{
var automations = _AutomationDatabaseService.GetByProvider(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)
{
// 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(userId, 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(userId, action.ProviderId);
// Check if device exist
if (actionDeviceToTest != null && providerAction != null)
{
switch (providerAction.Type)
{
case "zigbee2mqtt":
try
{
ActionOnZigbee2Mqtt(actionDeviceToTest, action, DeviceNameForAction);
} catch (Exception ex) {
System.Console.WriteLine($"ActionOnZigbee2Mqtt result in error: {ex}");
}
break;
case "meross":
try
{
ActionOnMeross(_DeviceDatabaseService, actionDeviceToTest, action, DeviceNameForAction);
}
catch (Exception ex)
{
System.Console.WriteLine($"ActionOnMeross result in error: {ex}");
}
break;
case "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");
}
}
}
}
}
// 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 - {deviceTrigger}");
}
}
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<State>();
if (actionDeviceToTest.LastState != null)
{
dic = JsonConvert.DeserializeObject<List<State>>(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))
{
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}");
}
}
// TODO clean this
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}");
}
}
}
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;
}
}
public static async Task UpdateZigbee2MqttConfigOrStateAsync(string topic, string message, string userId, DeviceDatabaseService _DeviceDatabaseService, GroupDatabaseService _GroupDatabaseService, ProviderDatabaseService _ProviderDatabaseService, LocationDatabaseService _LocationDatabaseService) {
// update zigbee2mqqtt config
switch (topic)
{
case "zigbee2mqtt/bridge/config/devices":
try
{
var devices = JsonConvert.DeserializeObject<List<Zigbee2MqttDevice>>(message);
var zigbee2mqttProvider = _ProviderDatabaseService.GetByType("zigbee2mqtt");
if (zigbee2mqttProvider != null)
{
// Retrieve existing devices
List<Device> existingDevices = _DeviceDatabaseService.GetByProviderId(zigbee2mqttProvider.Id);
var existingDevicesAddresses = existingDevices.Select(ed => ed.ServiceIdentification);
// Filter devices and check if something new
var filteredDevices = devices.Where(d => !existingDevicesAddresses.Contains(d.ieee_address)).ToList();
// Add new devices
Dictionary<string, List<DeviceDetailDTO>> createdDevices = await DeviceService.CreateFromZigbeeAsync(_DeviceDatabaseService, _ProviderDatabaseService, _LocationDatabaseService, userId, filteredDevices, zigbee2mqttProvider);
}
System.Console.WriteLine($"Devices updated for user {userId}");
}
catch (Exception ex)
{
Console.WriteLine($"Error during retrieving devices ! Exception: {ex}");
}
break;
case "zigbee2mqtt/bridge/groups":
try
{
var groupsConvert = JsonConvert.DeserializeObject<List<Zigbee2MqttGroup>>(message);
var zigbee2mqttProvider = _ProviderDatabaseService.GetByType("zigbee2mqtt");
if (zigbee2mqttProvider != null)
{
var groups = _GroupDatabaseService.GetByType(userId, "zigbee2mqtt");
// Compare the groups from MyCore and the group we received, if something diff in one group => Hard refresh (delete, new)
// TODO : Test with new devices
GroupService.CompareGroupsFromZigbee2Mqtt(userId, groups, groupsConvert, _DeviceDatabaseService, _GroupDatabaseService);
}
}
catch (Exception ex)
{
Console.WriteLine($"Error during retrieving groups ! Exception: {ex}");
}
break;
case "zigbee2mqtt/bridge/event":
try
{
var eventConvert = JsonConvert.DeserializeObject<Zigbee2MqttEvent>(message);
if (eventConvert.data?.ieee_address != null) {
var zigbeeDevice = _DeviceDatabaseService.GetByServiceIdentification(eventConvert.data.ieee_address);
if (zigbeeDevice != null && eventConvert.type == "device_announce") // Check if we can not hardcode that..
{
var status = new List<State>();
var state = new State();
state.Name = "state";
state.Value = "on";
status.Add(state);
var buildRequest = new Dictionary<string, object>();
buildRequest.Add(state.Name, state.Value);
// Update device state !
zigbeeDevice.LastState = JsonConvert.SerializeObject(buildRequest);
zigbeeDevice.LastStateDate = DateTime.Now;
_DeviceDatabaseService.Update(zigbeeDevice);
// Check if all group has the same state // Not needed as we test the first device of a group
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error during retrieving event ! Exception: {ex}");
}
break;
}
}
}
}