From cb24322bef1ab0d8e4882f074557c64860b1e537 Mon Sep 17 00:00:00 2001 From: Ryan Wagoner Date: Fri, 13 Dec 2019 23:14:20 -0500 Subject: [PATCH] - Refactor MQTT parser and add unit tests --- OmniLinkBridge/MQTT/Alarm.cs | 8 +- OmniLinkBridge/MQTT/AreaCommands.cs | 14 + OmniLinkBridge/MQTT/AreaState.cs | 8 +- OmniLinkBridge/MQTT/BinarySensor.cs | 5 - OmniLinkBridge/MQTT/Climate.cs | 6 +- OmniLinkBridge/MQTT/CommandTypes.cs | 11 + OmniLinkBridge/MQTT/Device.cs | 5 - OmniLinkBridge/MQTT/DeviceRegistry.cs | 8 +- OmniLinkBridge/MQTT/Light.cs | 8 +- OmniLinkBridge/MQTT/MappingExtensions.cs | 9 +- OmniLinkBridge/MQTT/MessageProcessor.cs | 185 ++++++++ OmniLinkBridge/MQTT/OverrideZone.cs | 8 +- OmniLinkBridge/MQTT/Sensor.cs | 5 - OmniLinkBridge/MQTT/Switch.cs | 8 +- OmniLinkBridge/MQTT/Topic.cs | 29 ++ OmniLinkBridge/MQTT/Topics.cs | 59 --- OmniLinkBridge/MQTT/UnitCommands.cs | 8 + OmniLinkBridge/MQTT/ZoneCommands.cs | 8 + OmniLinkBridge/Modules/MQTTModule.cs | 396 +++++------------- OmniLinkBridge/Modules/OmniLinkII.cs | 9 +- OmniLinkBridge/OmniLink/IOmniLinkII.cs | 11 + OmniLinkBridge/OmniLinkBridge.csproj | 8 +- OmniLinkBridgeTest/MQTTTest.cs | 159 +++++++ OmniLinkBridgeTest/Mock/MockOmniLinkII.cs | 30 ++ .../Mock/SendCommandEventArgs.cs | 33 ++ OmniLinkBridgeTest/OmniLinkBridgeTest.csproj | 7 + 26 files changed, 630 insertions(+), 415 deletions(-) create mode 100644 OmniLinkBridge/MQTT/AreaCommands.cs create mode 100644 OmniLinkBridge/MQTT/CommandTypes.cs create mode 100644 OmniLinkBridge/MQTT/MessageProcessor.cs create mode 100644 OmniLinkBridge/MQTT/Topic.cs delete mode 100644 OmniLinkBridge/MQTT/Topics.cs create mode 100644 OmniLinkBridge/MQTT/UnitCommands.cs create mode 100644 OmniLinkBridge/MQTT/ZoneCommands.cs create mode 100644 OmniLinkBridge/OmniLink/IOmniLinkII.cs create mode 100644 OmniLinkBridgeTest/MQTTTest.cs create mode 100644 OmniLinkBridgeTest/Mock/MockOmniLinkII.cs create mode 100644 OmniLinkBridgeTest/Mock/SendCommandEventArgs.cs diff --git a/OmniLinkBridge/MQTT/Alarm.cs b/OmniLinkBridge/MQTT/Alarm.cs index 4a3679e..88e94aa 100644 --- a/OmniLinkBridge/MQTT/Alarm.cs +++ b/OmniLinkBridge/MQTT/Alarm.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace OmniLinkBridge.MQTT +namespace OmniLinkBridge.MQTT { public class Alarm : Device { diff --git a/OmniLinkBridge/MQTT/AreaCommands.cs b/OmniLinkBridge/MQTT/AreaCommands.cs new file mode 100644 index 0000000..15546fc --- /dev/null +++ b/OmniLinkBridge/MQTT/AreaCommands.cs @@ -0,0 +1,14 @@ +namespace OmniLinkBridge.MQTT +{ + enum AreaCommands + { + disarm, + arm_home, + arm_away, + arm_night, + // The below aren't supported by Home Assistant + arm_home_instant, + arm_night_delay, + arm_vacation + } +} diff --git a/OmniLinkBridge/MQTT/AreaState.cs b/OmniLinkBridge/MQTT/AreaState.cs index 7e90746..2f24757 100644 --- a/OmniLinkBridge/MQTT/AreaState.cs +++ b/OmniLinkBridge/MQTT/AreaState.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace OmniLinkBridge.MQTT +namespace OmniLinkBridge.MQTT { public class AreaState { diff --git a/OmniLinkBridge/MQTT/BinarySensor.cs b/OmniLinkBridge/MQTT/BinarySensor.cs index 7565b02..dc8521a 100644 --- a/OmniLinkBridge/MQTT/BinarySensor.cs +++ b/OmniLinkBridge/MQTT/BinarySensor.cs @@ -1,10 +1,5 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace OmniLinkBridge.MQTT { diff --git a/OmniLinkBridge/MQTT/Climate.cs b/OmniLinkBridge/MQTT/Climate.cs index 5e4a9f6..2efd626 100644 --- a/OmniLinkBridge/MQTT/Climate.cs +++ b/OmniLinkBridge/MQTT/Climate.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Collections.Generic; namespace OmniLinkBridge.MQTT { diff --git a/OmniLinkBridge/MQTT/CommandTypes.cs b/OmniLinkBridge/MQTT/CommandTypes.cs new file mode 100644 index 0000000..657c92c --- /dev/null +++ b/OmniLinkBridge/MQTT/CommandTypes.cs @@ -0,0 +1,11 @@ +namespace OmniLinkBridge.MQTT +{ + enum CommandTypes + { + area, + zone, + unit, + thermostat, + button + } +} diff --git a/OmniLinkBridge/MQTT/Device.cs b/OmniLinkBridge/MQTT/Device.cs index e917365..60256a0 100644 --- a/OmniLinkBridge/MQTT/Device.cs +++ b/OmniLinkBridge/MQTT/Device.cs @@ -1,10 +1,5 @@ using Newtonsoft.Json; using OmniLinkBridge.Modules; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace OmniLinkBridge.MQTT { diff --git a/OmniLinkBridge/MQTT/DeviceRegistry.cs b/OmniLinkBridge/MQTT/DeviceRegistry.cs index 4669311..4b6e843 100644 --- a/OmniLinkBridge/MQTT/DeviceRegistry.cs +++ b/OmniLinkBridge/MQTT/DeviceRegistry.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace OmniLinkBridge.MQTT +namespace OmniLinkBridge.MQTT { public class DeviceRegistry { diff --git a/OmniLinkBridge/MQTT/Light.cs b/OmniLinkBridge/MQTT/Light.cs index f09862e..6ffff7f 100644 --- a/OmniLinkBridge/MQTT/Light.cs +++ b/OmniLinkBridge/MQTT/Light.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace OmniLinkBridge.MQTT +namespace OmniLinkBridge.MQTT { public class Light : Device { diff --git a/OmniLinkBridge/MQTT/MappingExtensions.cs b/OmniLinkBridge/MQTT/MappingExtensions.cs index 6463a7d..17efa28 100644 --- a/OmniLinkBridge/MQTT/MappingExtensions.cs +++ b/OmniLinkBridge/MQTT/MappingExtensions.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using HAI_Shared; +using HAI_Shared; using Newtonsoft.Json; namespace OmniLinkBridge.MQTT @@ -376,7 +371,7 @@ namespace OmniLinkBridge.MQTT public static string ToState(this clsUnit unit) { - return unit.Status == 0 || unit.Status == 100 ? "OFF" : "ON"; + return unit.Status == 0 || unit.Status == 100 ? UnitCommands.OFF.ToString() : UnitCommands.ON.ToString(); } public static int ToBrightnessState(this clsUnit unit) diff --git a/OmniLinkBridge/MQTT/MessageProcessor.cs b/OmniLinkBridge/MQTT/MessageProcessor.cs new file mode 100644 index 0000000..3e9e7a3 --- /dev/null +++ b/OmniLinkBridge/MQTT/MessageProcessor.cs @@ -0,0 +1,185 @@ +using HAI_Shared; +using log4net; +using OmniLinkBridge.OmniLink; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace OmniLinkBridge.MQTT +{ + public class MessageProcessor + { + private static ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + private readonly Regex regexTopic = new Regex(Global.mqtt_prefix + "/([A-Za-z]+)([0-9]+)/(.*)", RegexOptions.Compiled); + + private IOmniLinkII OmniLink { get; set; } + + public MessageProcessor(IOmniLinkII omni) + { + OmniLink = omni; + } + + public void Process(string messageTopic, string payload) + { + Match match = regexTopic.Match(messageTopic); + + if (!match.Success) + return; + + if (!Enum.TryParse(match.Groups[1].Value, true, out CommandTypes type) + || !Enum.TryParse(match.Groups[3].Value, true, out Topic topic) + || !ushort.TryParse(match.Groups[2].Value, out ushort id)) + return; + + log.Debug($"Received: Type: {type.ToString()}, Id: {id}, Command: {topic.ToString()}, Value: {payload}"); + + if (type == CommandTypes.area && id <= OmniLink.Controller.Areas.Count) + ProcessAreaReceived(OmniLink.Controller.Areas[id], topic, payload); + else if (type == CommandTypes.zone && id > 0 && id <= OmniLink.Controller.Zones.Count) + ProcessZoneReceived(OmniLink.Controller.Zones[id], topic, payload); + else if (type == CommandTypes.unit && id > 0 && id <= OmniLink.Controller.Units.Count) + ProcessUnitReceived(OmniLink.Controller.Units[id], topic, payload); + else if (type == CommandTypes.thermostat && id > 0 && id <= OmniLink.Controller.Thermostats.Count) + ProcessThermostatReceived(OmniLink.Controller.Thermostats[id], topic, payload); + else if (type == CommandTypes.button && id > 0 && id <= OmniLink.Controller.Buttons.Count) + ProcessButtonReceived(OmniLink.Controller.Buttons[id], topic, payload); + } + + private IDictionary AreaMapping = new Dictionary + { + { AreaCommands.disarm, enuUnitCommand.SecurityOff }, + { AreaCommands.arm_home, enuUnitCommand.SecurityDay }, + { AreaCommands.arm_away, enuUnitCommand.SecurityAway }, + { AreaCommands.arm_night, enuUnitCommand.SecurityNight }, + // The below aren't supported by Home Assistant + { AreaCommands.arm_home_instant, enuUnitCommand.SecurityDyi }, + { AreaCommands.arm_night_delay, enuUnitCommand.SecurityNtd }, + { AreaCommands.arm_vacation, enuUnitCommand.SecurityVac }, + }; + + private void ProcessAreaReceived(clsArea area, Topic command, string payload) + { + if (command == Topic.command && Enum.TryParse(payload, true, out AreaCommands cmd)) + { + if (area.Number == 0) + log.Debug("SetArea: 0 implies all areas will be changed"); + + log.Debug("SetArea: " + area.Number + " to " + cmd.ToString().Replace("arm_", "").Replace("_", " ")); + OmniLink.SendCommand(AreaMapping[cmd], 0, (ushort)area.Number); + } + } + + private IDictionary ZoneMapping = new Dictionary + { + { ZoneCommands.restore, enuUnitCommand.Restore }, + { ZoneCommands.bypass, enuUnitCommand.Bypass }, + }; + + private void ProcessZoneReceived(clsZone zone, Topic command, string payload) + { + if (command == Topic.command && Enum.TryParse(payload, true, out ZoneCommands cmd)) + { + log.Debug("SetZone: " + zone.Number + " to " + payload); + OmniLink.SendCommand(ZoneMapping[cmd], 0, (ushort)zone.Number); + } + } + + private IDictionary UnitMapping = new Dictionary + { + { UnitCommands.OFF, enuUnitCommand.Off }, + { UnitCommands.ON, enuUnitCommand.On } + }; + + private void ProcessUnitReceived(clsUnit unit, Topic command, string payload) + { + if (command == Topic.command && Enum.TryParse(payload, true, out UnitCommands cmd)) + { + if (string.Compare(unit.ToState(), cmd.ToString()) != 0) + { + log.Debug("SetUnit: " + unit.Number + " to " + cmd.ToString()); + OmniLink.SendCommand(UnitMapping[cmd], 0, (ushort)unit.Number); + } + } + else if (command == Topic.brightness_command && int.TryParse(payload, out int unitValue)) + { + log.Debug("SetUnit: " + unit.Number + " to " + payload + "%"); + + OmniLink.SendCommand(enuUnitCommand.Level, BitConverter.GetBytes(unitValue)[0], (ushort)unit.Number); + + // Force status change instead of waiting on controller to update + // Home Assistant sends brightness immediately followed by ON, + // which will cause light to go to 100% brightness + unit.Status = (byte)(100 + unitValue); + } + } + + private void ProcessThermostatReceived(clsThermostat thermostat, Topic command, string payload) + { + if (command == Topic.temperature_heat_command && double.TryParse(payload, out double tempLow)) + { + string tempUnit = "C"; + if (OmniLink.Controller.TempFormat == enuTempFormat.Fahrenheit) + { + tempLow = tempLow.ToCelsius(); + tempUnit = "F"; + } + + int temp = tempLow.ToOmniTemp(); + log.Debug("SetThermostatHeatSetpoint: " + thermostat.Number + " to " + payload + tempUnit + "(" + temp + ")"); + OmniLink.SendCommand(enuUnitCommand.SetLowSetPt, BitConverter.GetBytes(temp)[0], (ushort)thermostat.Number); + } + else if (command == Topic.temperature_cool_command && double.TryParse(payload, out double tempHigh)) + { + string tempUnit = "C"; + if (OmniLink.Controller.TempFormat == enuTempFormat.Fahrenheit) + { + tempHigh = tempHigh.ToCelsius(); + tempUnit = "F"; + } + + int temp = tempHigh.ToOmniTemp(); + log.Debug("SetThermostatCoolSetpoint: " + thermostat.Number + " to " + payload + tempUnit + "(" + temp + ")"); + OmniLink.SendCommand(enuUnitCommand.SetHighSetPt, BitConverter.GetBytes(temp)[0], (ushort)thermostat.Number); + } + else if (command == Topic.humidify_command && double.TryParse(payload, out double humidify)) + { + // Humidity is reported where Fahrenheit temperatures 0-100 correspond to 0-100% relative humidity + int level = humidify.ToCelsius().ToOmniTemp(); + log.Debug("SetThermostatHumidifySetpoint: " + thermostat.Number + " to " + payload + "% (" + level + ")"); + OmniLink.SendCommand(enuUnitCommand.SetHumidifySetPt, BitConverter.GetBytes(level)[0], (ushort)thermostat.Number); + } + else if (command == Topic.dehumidify_command && double.TryParse(payload, out double dehumidify)) + { + int level = dehumidify.ToCelsius().ToOmniTemp(); + log.Debug("SetThermostatDehumidifySetpoint: " + thermostat.Number + " to " + payload + "% (" + level + ")"); + OmniLink.SendCommand(enuUnitCommand.SetDeHumidifySetPt, BitConverter.GetBytes(level)[0], (ushort)thermostat.Number); + } + else if (command == Topic.mode_command && Enum.TryParse(payload, true, out enuThermostatMode mode)) + { + log.Debug("SetThermostatMode: " + thermostat.Number + " to " + payload); + OmniLink.SendCommand(enuUnitCommand.Mode, BitConverter.GetBytes((int)mode)[0], (ushort)thermostat.Number); + } + else if (command == Topic.fan_mode_command && Enum.TryParse(payload, true, out enuThermostatFanMode fanMode)) + { + log.Debug("SetThermostatFanMode: " + thermostat.Number + " to " + payload); + OmniLink.SendCommand(enuUnitCommand.Fan, BitConverter.GetBytes((int)fanMode)[0], (ushort)thermostat.Number); + } + else if (command == Topic.hold_command && Enum.TryParse(payload, true, out enuThermostatHoldMode holdMode)) + { + log.Debug("SetThermostatHold: " + thermostat.Number + " to " + payload); + OmniLink.SendCommand(enuUnitCommand.Hold, BitConverter.GetBytes((int)holdMode)[0], (ushort)thermostat.Number); + } + } + + private void ProcessButtonReceived(clsButton button, Topic command, string payload) + { + if (command == Topic.command && Enum.TryParse(payload, true, out UnitCommands cmd) && cmd == UnitCommands.ON) + { + log.Debug("PushButton: " + button.Number); + OmniLink.SendCommand(enuUnitCommand.Button, 0, (ushort)button.Number); + } + } + } +} diff --git a/OmniLinkBridge/MQTT/OverrideZone.cs b/OmniLinkBridge/MQTT/OverrideZone.cs index e7492d4..0d00e9d 100644 --- a/OmniLinkBridge/MQTT/OverrideZone.cs +++ b/OmniLinkBridge/MQTT/OverrideZone.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace OmniLinkBridge.MQTT +namespace OmniLinkBridge.MQTT { public class OverrideZone { diff --git a/OmniLinkBridge/MQTT/Sensor.cs b/OmniLinkBridge/MQTT/Sensor.cs index 717ab17..0d11500 100644 --- a/OmniLinkBridge/MQTT/Sensor.cs +++ b/OmniLinkBridge/MQTT/Sensor.cs @@ -1,10 +1,5 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace OmniLinkBridge.MQTT { diff --git a/OmniLinkBridge/MQTT/Switch.cs b/OmniLinkBridge/MQTT/Switch.cs index f44c8a7..55ed155 100644 --- a/OmniLinkBridge/MQTT/Switch.cs +++ b/OmniLinkBridge/MQTT/Switch.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace OmniLinkBridge.MQTT +namespace OmniLinkBridge.MQTT { public class Switch : Device { diff --git a/OmniLinkBridge/MQTT/Topic.cs b/OmniLinkBridge/MQTT/Topic.cs new file mode 100644 index 0000000..10c5d93 --- /dev/null +++ b/OmniLinkBridge/MQTT/Topic.cs @@ -0,0 +1,29 @@ +namespace OmniLinkBridge.MQTT +{ + public enum Topic + { + state, + command, + basic_state, + json_state, + brightness_state, + brightness_command, + current_operation, + current_temperature, + current_humidity, + temperature_heat_state, + temperature_heat_command, + temperature_cool_state, + temperature_cool_command, + humidify_state, + humidify_command, + dehumidify_state, + dehumidify_command, + mode_state, + mode_command, + fan_mode_state, + fan_mode_command, + hold_state, + hold_command + } +} diff --git a/OmniLinkBridge/MQTT/Topics.cs b/OmniLinkBridge/MQTT/Topics.cs deleted file mode 100644 index cd1858e..0000000 --- a/OmniLinkBridge/MQTT/Topics.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace OmniLinkBridge.MQTT -{ - public class Topic - { - public string Value { get; private set; } - - private Topic(string value) - { - Value = value; - } - - public override string ToString() - { - return Value; - } - - public static Topic state { get { return new Topic("state"); } } - public static Topic command { get { return new Topic("command"); } } - - public static Topic basic_state { get { return new Topic("basic_state"); } } - public static Topic json_state { get { return new Topic("json_state"); } } - - public static Topic brightness_state { get { return new Topic("brightness_state"); } } - public static Topic brightness_command { get { return new Topic("brightness_command"); } } - - public static Topic current_operation { get { return new Topic("current_operation"); } } - public static Topic current_temperature { get { return new Topic("current_temperature"); } } - public static Topic current_humidity { get { return new Topic("current_humidity"); } } - - public static Topic temperature_heat_state { get { return new Topic("temperature_heat_state"); } } - public static Topic temperature_heat_command { get { return new Topic("temperature_heat_command"); } } - - public static Topic temperature_cool_state { get { return new Topic("temperature_cool_state"); } } - public static Topic temperature_cool_command { get { return new Topic("temperature_cool_command"); } } - - public static Topic humidify_state { get { return new Topic("humidify_state"); } } - public static Topic humidify_command { get { return new Topic("humidify_command"); } } - - public static Topic dehumidify_state { get { return new Topic("dehumidify_state"); } } - public static Topic dehumidify_command { get { return new Topic("dehumidify_command"); } } - - public static Topic mode_state { get { return new Topic("mode_state"); } } - public static Topic mode_command { get { return new Topic("mode_command"); } } - - public static Topic fan_mode_state { get { return new Topic("fan_mode_state"); } } - public static Topic fan_mode_command { get { return new Topic("fan_mode_command"); } } - - public static Topic hold_state { get { return new Topic("hold_state"); } } - public static Topic hold_command { get { return new Topic("hold_command"); } } - } -} diff --git a/OmniLinkBridge/MQTT/UnitCommands.cs b/OmniLinkBridge/MQTT/UnitCommands.cs new file mode 100644 index 0000000..bcb7b78 --- /dev/null +++ b/OmniLinkBridge/MQTT/UnitCommands.cs @@ -0,0 +1,8 @@ +namespace OmniLinkBridge.MQTT +{ + enum UnitCommands + { + OFF, + ON + } +} diff --git a/OmniLinkBridge/MQTT/ZoneCommands.cs b/OmniLinkBridge/MQTT/ZoneCommands.cs new file mode 100644 index 0000000..75d540c --- /dev/null +++ b/OmniLinkBridge/MQTT/ZoneCommands.cs @@ -0,0 +1,8 @@ +namespace OmniLinkBridge.MQTT +{ + enum ZoneCommands + { + restore, + bypass, + } +} diff --git a/OmniLinkBridge/Modules/MQTTModule.cs b/OmniLinkBridge/Modules/MQTTModule.cs index b19e24a..a9850a8 100644 --- a/OmniLinkBridge/Modules/MQTTModule.cs +++ b/OmniLinkBridge/Modules/MQTTModule.cs @@ -8,10 +8,11 @@ using Newtonsoft.Json; using OmniLinkBridge.MQTT; using OmniLinkBridge.OmniLink; using System; +using System.Collections.Generic; using System.Reflection; using System.Text; -using System.Text.RegularExpressions; using System.Threading; +using System.Threading.Tasks; namespace OmniLinkBridge.Modules { @@ -25,7 +26,7 @@ namespace OmniLinkBridge.Modules private IManagedMqttClient MqttClient { get; set; } private bool ControllerConnected { get; set; } - private Regex regexTopic = new Regex(Global.mqtt_prefix + "/([A-Za-z]+)([0-9]+)/(.*)", RegexOptions.Compiled); + private MessageProcessor MessageProcessor { get; set; } private readonly AutoResetEvent trigger = new AutoResetEvent(false); @@ -38,6 +39,8 @@ namespace OmniLinkBridge.Modules OmniLink.OnZoneStatus += Omnilink_OnZoneStatus; OmniLink.OnUnitStatus += Omnilink_OnUnitStatus; OmniLink.OnThermostatStatus += Omnilink_OnThermostatStatus; + + MessageProcessor = new MessageProcessor(omni); } public void Startup() @@ -79,7 +82,7 @@ namespace OmniLinkBridge.Modules // For the initial connection wait for the controller connected event to publish config // For subsequent connections publish config immediately - if(ControllerConnected) + if (ControllerConnected) PublishConfig(); }; MqttClient.ConnectingFailed += (sender, e) => { log.Debug("Error connecting " + e.Exception.Message); }; @@ -89,218 +92,35 @@ namespace OmniLinkBridge.Modules MqttClient.ApplicationMessageReceived += MqttClient_ApplicationMessageReceived; - MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic($"{Global.mqtt_prefix}/+/{Topic.command}").Build()); - MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic($"{Global.mqtt_prefix}/+/{Topic.brightness_command}").Build()); - MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic($"{Global.mqtt_prefix}/+/{Topic.temperature_heat_command}").Build()); - MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic($"{Global.mqtt_prefix}/+/{Topic.temperature_cool_command}").Build()); - MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic($"{Global.mqtt_prefix}/+/{Topic.humidify_command}").Build()); - MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic($"{Global.mqtt_prefix}/+/{Topic.dehumidify_command}").Build()); - MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic($"{Global.mqtt_prefix}/+/{Topic.mode_command}").Build()); - MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic($"{Global.mqtt_prefix}/+/{Topic.fan_mode_command}").Build()); - MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic($"{Global.mqtt_prefix}/+/{Topic.hold_command}").Build()); + // Subscribe to notifications for these command topics + List toSubscribe = new List() + { + Topic.command, + Topic.brightness_command, + Topic.temperature_heat_command, + Topic.temperature_cool_command, + Topic.humidify_command, + Topic.dehumidify_command, + Topic.mode_command, + Topic.fan_mode_command, + Topic.hold_command + }; + + toSubscribe.ForEach((command) => MqttClient.SubscribeAsync( + new TopicFilterBuilder().WithTopic($"{Global.mqtt_prefix}/+/{command.ToString()}").Build())); // Wait until shutdown trigger.WaitOne(); log.Debug("Publishing controller offline"); - MqttClient.PublishAsync($"{Global.mqtt_prefix}/status", "offline", MqttQualityOfServiceLevel.AtMostOnce, true); + PublishAsync($"{Global.mqtt_prefix}/status", "offline"); MqttClient.StopAsync(); } private void MqttClient_ApplicationMessageReceived(object sender, MqttApplicationMessageReceivedEventArgs e) { - Match match = regexTopic.Match(e.ApplicationMessage.Topic); - - if (!match.Success) - return; - - string payload = Encoding.UTF8.GetString(e.ApplicationMessage.Payload); - - log.Debug($"Received: Type: {match.Groups[1].Value}, Id: {match.Groups[2].Value}, Command: {match.Groups[3].Value}, Value: {payload}"); - - if (match.Groups[1].Value == "area" && ushort.TryParse(match.Groups[2].Value, out ushort areaId) && - areaId <= OmniLink.Controller.Areas.Count) - { - if(areaId == 0) - log.Debug("SetArea: 0 implies all areas will be changed"); - - ProcessAreaReceived(OmniLink.Controller.Areas[areaId], match.Groups[3].Value, payload); - } - if (match.Groups[1].Value == "zone" && ushort.TryParse(match.Groups[2].Value, out ushort zoneId) && - zoneId > 0 && zoneId <= OmniLink.Controller.Zones.Count) - { - ProcessZoneReceived(OmniLink.Controller.Zones[zoneId], match.Groups[3].Value, payload); - } - else if (match.Groups[1].Value == "unit" && ushort.TryParse(match.Groups[2].Value, out ushort unitId) && - unitId > 0 && unitId <= OmniLink.Controller.Units.Count) - { - ProcessUnitReceived(OmniLink.Controller.Units[unitId], match.Groups[3].Value, payload); - } - else if (match.Groups[1].Value == "thermostat" && ushort.TryParse(match.Groups[2].Value, out ushort thermostatId) && - thermostatId > 0 && thermostatId <= OmniLink.Controller.Thermostats.Count) - { - ProcessThermostatReceived(OmniLink.Controller.Thermostats[thermostatId], match.Groups[3].Value, payload); - } - else if (match.Groups[1].Value == "button" && ushort.TryParse(match.Groups[2].Value, out ushort buttonId) && - buttonId > 0 && buttonId <= OmniLink.Controller.Buttons.Count) - { - ProcessButtonReceived(OmniLink.Controller.Buttons[buttonId], match.Groups[3].Value, payload); - } - } - - private void ProcessAreaReceived(clsArea area, string command, string payload) - { - if (string.Compare(command, Topic.command.ToString()) == 0) - { - if(string.Compare(payload, "arm_home", true) == 0) - { - log.Debug("SetArea: " + area.Number + " to home"); - OmniLink.Controller.SendCommand(enuUnitCommand.SecurityDay, 0, (ushort)area.Number); - } - else if (string.Compare(payload, "arm_away", true) == 0) - { - log.Debug("SetArea: " + area.Number + " to away"); - OmniLink.Controller.SendCommand(enuUnitCommand.SecurityAway, 0, (ushort)area.Number); - } - else if (string.Compare(payload, "arm_night", true) == 0) - { - log.Debug("SetArea: " + area.Number + " to night"); - OmniLink.Controller.SendCommand(enuUnitCommand.SecurityNight, 0, (ushort)area.Number); - } - else if (string.Compare(payload, "disarm", true) == 0) - { - log.Debug("SetArea: " + area.Number + " to disarm"); - OmniLink.Controller.SendCommand(enuUnitCommand.SecurityOff, 0, (ushort)area.Number); - } - // The below aren't supported by Home Assistant - else if (string.Compare(payload, "arm_home_instant", true) == 0) - { - log.Debug("SetArea: " + area.Number + " to home instant"); - OmniLink.Controller.SendCommand(enuUnitCommand.SecurityDyi, 0, (ushort)area.Number); - } - else if (string.Compare(payload, "arm_night_delay", true) == 0) - { - log.Debug("SetArea: " + area.Number + " to night delay"); - OmniLink.Controller.SendCommand(enuUnitCommand.SecurityNtd, 0, (ushort)area.Number); - } - else if (string.Compare(payload, "arm_vacation", true) == 0) - { - log.Debug("SetArea: " + area.Number + " to vacation"); - OmniLink.Controller.SendCommand(enuUnitCommand.SecurityVac, 0, (ushort)area.Number); - } - } - } - - private void ProcessZoneReceived(clsZone zone, string command, string payload) - { - if (string.Compare(command, Topic.command.ToString()) == 0) - { - if (string.Compare(payload, "bypass", true) == 0) - { - log.Debug("SetZone: " + zone.Number + " to " + payload); - OmniLink.Controller.SendCommand(enuUnitCommand.Bypass, 0, (ushort)zone.Number); - } - else if (string.Compare(payload, "restore", true) == 0) - { - log.Debug("SetZone: " + zone.Number + " to " + payload); - OmniLink.Controller.SendCommand(enuUnitCommand.Restore, 0, (ushort)zone.Number); - } - } - } - - private void ProcessUnitReceived(clsUnit unit, string command, string payload) - { - if (string.Compare(command, Topic.command.ToString()) == 0 && (payload == "ON" || payload == "OFF")) - { - if (unit.ToState() != payload) - { - log.Debug("SetUnit: " + unit.Number + " to " + payload); - - if (payload == "ON") - OmniLink.Controller.SendCommand(enuUnitCommand.On, 0, (ushort)unit.Number); - else - OmniLink.Controller.SendCommand(enuUnitCommand.Off, 0, (ushort)unit.Number); - } - } - else if (string.Compare(command, Topic.brightness_command.ToString()) == 0 && Int32.TryParse(payload, out int unitValue)) - { - log.Debug("SetUnit: " + unit.Number + " to " + payload + "%"); - - OmniLink.Controller.SendCommand(enuUnitCommand.Level, BitConverter.GetBytes(unitValue)[0], (ushort)unit.Number); - - // Force status change instead of waiting on controller to update - // Home Assistant sends brightness immediately followed by ON, - // which will cause light to go to 100% brightness - unit.Status = (byte)(100 + unitValue); - } - } - - private void ProcessThermostatReceived(clsThermostat thermostat, string command, string payload) - { - if (string.Compare(command, Topic.temperature_heat_command.ToString()) == 0 && double.TryParse(payload, out double tempLow)) - { - string tempUnit = "C"; - if (OmniLink.Controller.TempFormat == enuTempFormat.Fahrenheit) - { - tempLow = tempLow.ToCelsius(); - tempUnit = "F"; - } - - int temp = tempLow.ToOmniTemp(); - log.Debug("SetThermostatHeatSetpoint: " + thermostat.Number + " to " + payload + tempUnit + "(" + temp + ")"); - OmniLink.Controller.SendCommand(enuUnitCommand.SetLowSetPt, BitConverter.GetBytes(temp)[0], (ushort)thermostat.Number); - } - else if (string.Compare(command, Topic.temperature_cool_command.ToString()) == 0 && double.TryParse(payload, out double tempHigh)) - { - string tempUnit = "C"; - if (OmniLink.Controller.TempFormat == enuTempFormat.Fahrenheit) - { - tempHigh = tempHigh.ToCelsius(); - tempUnit = "F"; - } - - int temp = tempHigh.ToOmniTemp(); - log.Debug("SetThermostatCoolSetpoint: " + thermostat.Number + " to " + payload + tempUnit + "(" + temp + ")"); - OmniLink.Controller.SendCommand(enuUnitCommand.SetHighSetPt, BitConverter.GetBytes(temp)[0], (ushort)thermostat.Number); - } - else if (string.Compare(command, Topic.humidify_command.ToString()) == 0 && double.TryParse(payload, out double humidify)) - { - // Humidity is reported where Fahrenheit temperatures 0-100 correspond to 0-100% relative humidity - int level = humidify.ToCelsius().ToOmniTemp(); - log.Debug("SetThermostatHumidifySetpoint: " + thermostat.Number + " to " + payload + "% (" + level + ")"); - OmniLink.Controller.SendCommand(enuUnitCommand.SetHumidifySetPt, BitConverter.GetBytes(level)[0], (ushort)thermostat.Number); - } - else if (string.Compare(command, Topic.dehumidify_command.ToString()) == 0 && double.TryParse(payload, out double dehumidify)) - { - int level = dehumidify.ToCelsius().ToOmniTemp(); - log.Debug("SetThermostatDehumidifySetpoint: " + thermostat.Number + " to " + payload + "% (" + level + ")"); - OmniLink.Controller.SendCommand(enuUnitCommand.SetDeHumidifySetPt, BitConverter.GetBytes(level)[0], (ushort)thermostat.Number); - } - else if (string.Compare(command, Topic.mode_command.ToString()) == 0 && Enum.TryParse(payload, true, out enuThermostatMode mode)) - { - log.Debug("SetThermostatMode: " + thermostat.Number + " to " + payload); - OmniLink.Controller.SendCommand(enuUnitCommand.Mode, BitConverter.GetBytes((int)mode)[0], (ushort)thermostat.Number); - } - else if (string.Compare(command, Topic.fan_mode_command.ToString()) == 0 && Enum.TryParse(payload, true, out enuThermostatFanMode fanMode)) - { - log.Debug("SetThermostatFanMode: " + thermostat.Number + " to " + payload); - OmniLink.Controller.SendCommand(enuUnitCommand.Fan, BitConverter.GetBytes((int)fanMode)[0], (ushort)thermostat.Number); - } - else if (string.Compare(command, Topic.hold_command.ToString()) == 0 && Enum.TryParse(payload, true, out enuThermostatHoldMode holdMode)) - { - log.Debug("SetThermostatHold: " + thermostat.Number + " to " + payload); - OmniLink.Controller.SendCommand(enuUnitCommand.Hold, BitConverter.GetBytes((int)holdMode)[0], (ushort)thermostat.Number); - } - } - - private void ProcessButtonReceived(clsButton button, string command, string payload) - { - if (string.Compare(command, Topic.command.ToString()) == 0 && payload == "ON") - { - log.Debug("PushButton: " + button.Number); - OmniLink.Controller.SendCommand(enuUnitCommand.Button, 0, (ushort)button.Number); - } + MessageProcessor.Process(e.ApplicationMessage.Topic, Encoding.UTF8.GetString(e.ApplicationMessage.Payload)); } public void Shutdown() @@ -323,7 +143,7 @@ namespace OmniLinkBridge.Modules if (MqttClient.IsConnected) { log.Debug("Publishing controller offline"); - MqttClient.PublishAsync($"{Global.mqtt_prefix}/status", "offline", MqttQualityOfServiceLevel.AtMostOnce, true); + PublishAsync($"{Global.mqtt_prefix}/status", "offline"); } } @@ -336,7 +156,9 @@ namespace OmniLinkBridge.Modules PublishButtons(); log.Debug("Publishing controller online"); - MqttClient.PublishAsync($"{Global.mqtt_prefix}/status", "online", MqttQualityOfServiceLevel.AtMostOnce, true); + PublishAsync($"{Global.mqtt_prefix}/status", "online"); + PublishAsync($"{Global.mqtt_prefix}/model", OmniLink.Controller.GetModelText()); + PublishAsync($"{Global.mqtt_prefix}/version", OmniLink.Controller.GetVersionText()); } private void PublishAreas() @@ -351,38 +173,38 @@ namespace OmniLinkBridge.Modules // (configured for 1 area). To workaround ignore default properties for the first area. if (i > 1 && area.DefaultProperties == true) { - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/alarm_control_panel/{Global.mqtt_prefix}/area{i.ToString()}/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}burglary/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}fire/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}gas/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}aux/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}freeze/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}water/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}duress/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}temp/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); + PublishAsync($"{Global.mqtt_discovery_prefix}/alarm_control_panel/{Global.mqtt_prefix}/area{i.ToString()}/config", null); + PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}burglary/config", null); + PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}fire/config", null); + PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}gas/config", null); + PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}aux/config", null); + PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}freeze/config", null); + PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}water/config", null); + PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}duress/config", null); + PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}temp/config", null); continue; } PublishAreaState(area); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/alarm_control_panel/{Global.mqtt_prefix}/area{i.ToString()}/config", - JsonConvert.SerializeObject(area.ToConfig()), MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}burglary/config", - JsonConvert.SerializeObject(area.ToConfigBurglary()), MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}fire/config", - JsonConvert.SerializeObject(area.ToConfigFire()), MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}gas/config", - JsonConvert.SerializeObject(area.ToConfigGas()), MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}aux/config", - JsonConvert.SerializeObject(area.ToConfigAux()), MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}freeze/config", - JsonConvert.SerializeObject(area.ToConfigFreeze()), MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}water/config", - JsonConvert.SerializeObject(area.ToConfigWater()), MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}duress/config", - JsonConvert.SerializeObject(area.ToConfigDuress()), MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}temp/config", - JsonConvert.SerializeObject(area.ToConfigTemp()), MqttQualityOfServiceLevel.AtMostOnce, true); + PublishAsync($"{Global.mqtt_discovery_prefix}/alarm_control_panel/{Global.mqtt_prefix}/area{i.ToString()}/config", + JsonConvert.SerializeObject(area.ToConfig())); + PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}burglary/config", + JsonConvert.SerializeObject(area.ToConfigBurglary())); + PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}fire/config", + JsonConvert.SerializeObject(area.ToConfigFire())); + PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}gas/config", + JsonConvert.SerializeObject(area.ToConfigGas())); + PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}aux/config", + JsonConvert.SerializeObject(area.ToConfigAux())); + PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}freeze/config", + JsonConvert.SerializeObject(area.ToConfigFreeze())); + PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}water/config", + JsonConvert.SerializeObject(area.ToConfigWater())); + PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}duress/config", + JsonConvert.SerializeObject(area.ToConfigDuress())); + PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/area{i.ToString()}temp/config", + JsonConvert.SerializeObject(area.ToConfigTemp())); } } @@ -396,26 +218,26 @@ namespace OmniLinkBridge.Modules if (zone.DefaultProperties == true || Global.mqtt_discovery_ignore_zones.Contains(zone.Number)) { - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/zone{i.ToString()}/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/zone{i.ToString()}/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/zone{i.ToString()}temp/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/zone{i.ToString()}humidity/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); + PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/zone{i.ToString()}/config", null); + PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/zone{i.ToString()}/config", null); + PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/zone{i.ToString()}temp/config", null); + PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/zone{i.ToString()}humidity/config", null); continue; } PublishZoneState(zone); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/zone{i.ToString()}/config", - JsonConvert.SerializeObject(zone.ToConfig()), MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/zone{i.ToString()}/config", - JsonConvert.SerializeObject(zone.ToConfigSensor()), MqttQualityOfServiceLevel.AtMostOnce, true); + PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/zone{i.ToString()}/config", + JsonConvert.SerializeObject(zone.ToConfig())); + PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/zone{i.ToString()}/config", + JsonConvert.SerializeObject(zone.ToConfigSensor())); if (zone.IsTemperatureZone()) - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/zone{i.ToString()}temp/config", - JsonConvert.SerializeObject(zone.ToConfigTemp(OmniLink.Controller.TempFormat)), MqttQualityOfServiceLevel.AtMostOnce, true); + PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/zone{i.ToString()}temp/config", + JsonConvert.SerializeObject(zone.ToConfigTemp(OmniLink.Controller.TempFormat))); else if (zone.IsHumidityZone()) - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/zone{i.ToString()}humidity/config", - JsonConvert.SerializeObject(zone.ToConfigHumidity()), MqttQualityOfServiceLevel.AtMostOnce, true); + PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/zone{i.ToString()}humidity/config", + JsonConvert.SerializeObject(zone.ToConfigHumidity())); } } @@ -430,18 +252,18 @@ namespace OmniLinkBridge.Modules if (unit.DefaultProperties == true || Global.mqtt_discovery_ignore_units.Contains(unit.Number)) { string type = i < 385 ? "light" : "switch"; - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/{type}/{Global.mqtt_prefix}/unit{i.ToString()}/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); + PublishAsync($"{Global.mqtt_discovery_prefix}/{type}/{Global.mqtt_prefix}/unit{i.ToString()}/config", null); continue; } PublishUnitState(unit); if(i < 385) - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/light/{Global.mqtt_prefix}/unit{i.ToString()}/config", - JsonConvert.SerializeObject(unit.ToConfig()), MqttQualityOfServiceLevel.AtMostOnce, true); + PublishAsync($"{Global.mqtt_discovery_prefix}/light/{Global.mqtt_prefix}/unit{i.ToString()}/config", + JsonConvert.SerializeObject(unit.ToConfig())); else - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/unit{i.ToString()}/config", - JsonConvert.SerializeObject(unit.ToConfigSwitch()), MqttQualityOfServiceLevel.AtMostOnce, true); + PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/unit{i.ToString()}/config", + JsonConvert.SerializeObject(unit.ToConfigSwitch())); } } @@ -455,20 +277,20 @@ namespace OmniLinkBridge.Modules if (thermostat.DefaultProperties == true) { - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/climate/{Global.mqtt_prefix}/thermostat{i.ToString()}/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/thermostat{i.ToString()}temp/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/thermostat{i.ToString()}humidity/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); + PublishAsync($"{Global.mqtt_discovery_prefix}/climate/{Global.mqtt_prefix}/thermostat{i.ToString()}/config", null); + PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/thermostat{i.ToString()}temp/config", null); + PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/thermostat{i.ToString()}humidity/config", null); continue; } PublishThermostatState(thermostat); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/climate/{Global.mqtt_prefix}/thermostat{i.ToString()}/config", - JsonConvert.SerializeObject(thermostat.ToConfig(OmniLink.Controller.TempFormat)), MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/thermostat{i.ToString()}temp/config", - JsonConvert.SerializeObject(thermostat.ToConfigTemp(OmniLink.Controller.TempFormat)), MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/thermostat{i.ToString()}humidity/config", - JsonConvert.SerializeObject(thermostat.ToConfigHumidity()), MqttQualityOfServiceLevel.AtMostOnce, true); + PublishAsync($"{Global.mqtt_discovery_prefix}/climate/{Global.mqtt_prefix}/thermostat{i.ToString()}/config", + JsonConvert.SerializeObject(thermostat.ToConfig(OmniLink.Controller.TempFormat))); + PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/thermostat{i.ToString()}temp/config", + JsonConvert.SerializeObject(thermostat.ToConfigTemp(OmniLink.Controller.TempFormat))); + PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/thermostat{i.ToString()}humidity/config", + JsonConvert.SerializeObject(thermostat.ToConfigHumidity())); } } @@ -482,15 +304,15 @@ namespace OmniLinkBridge.Modules if (button.DefaultProperties == true) { - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/button{i.ToString()}/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); + PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/button{i.ToString()}/config", null); continue; } // Buttons are always off - MqttClient.PublishAsync(button.ToTopic(Topic.state), "OFF", MqttQualityOfServiceLevel.AtMostOnce, true); + PublishAsync(button.ToTopic(Topic.state), "OFF"); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/button{i.ToString()}/config", - JsonConvert.SerializeObject(button.ToConfig()), MqttQualityOfServiceLevel.AtMostOnce, true); + PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/button{i.ToString()}/config", + JsonConvert.SerializeObject(button.ToConfig())); } } @@ -508,7 +330,8 @@ namespace OmniLinkBridge.Modules clsZone zone = OmniLink.Controller.Zones[i]; if (zone.DefaultProperties == false && zone.Area == e.Area.Number) - OmniLink.Controller.Connection.Send(new clsOL2MsgRequestExtendedStatus(OmniLink.Controller.Connection, enuObjectType.Zone, i, i), HandleRequestZoneStatus); + OmniLink.Controller.Connection.Send(new clsOL2MsgRequestExtendedStatus( + OmniLink.Controller.Connection, enuObjectType.Zone, i, i), HandleRequestZoneStatus); } } @@ -523,7 +346,7 @@ namespace OmniLinkBridge.Modules { clsZone zone = OmniLink.Controller.Zones[MSG.ObjectNumber(i)]; zone.CopyExtendedStatus(MSG, i); - MqttClient.PublishAsync(zone.ToTopic(Topic.state), zone.ToState(), MqttQualityOfServiceLevel.AtMostOnce, true); + PublishAsync(zone.ToTopic(Topic.state), zone.ToState()); } } @@ -556,42 +379,47 @@ namespace OmniLinkBridge.Modules private void PublishAreaState(clsArea area) { - MqttClient.PublishAsync(area.ToTopic(Topic.state), area.ToState(), MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync(area.ToTopic(Topic.basic_state), area.ToBasicState(), MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync(area.ToTopic(Topic.json_state), area.ToJsonState(), MqttQualityOfServiceLevel.AtMostOnce, true); + PublishAsync(area.ToTopic(Topic.state), area.ToState()); + PublishAsync(area.ToTopic(Topic.basic_state), area.ToBasicState()); + PublishAsync(area.ToTopic(Topic.json_state), area.ToJsonState()); } private void PublishZoneState(clsZone zone) { - MqttClient.PublishAsync(zone.ToTopic(Topic.state), zone.ToState(), MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync(zone.ToTopic(Topic.basic_state), zone.ToBasicState(), MqttQualityOfServiceLevel.AtMostOnce, true); + PublishAsync(zone.ToTopic(Topic.state), zone.ToState()); + PublishAsync(zone.ToTopic(Topic.basic_state), zone.ToBasicState()); if(zone.IsTemperatureZone()) - MqttClient.PublishAsync(zone.ToTopic(Topic.current_temperature), zone.TempText(), MqttQualityOfServiceLevel.AtMostOnce, true); + PublishAsync(zone.ToTopic(Topic.current_temperature), zone.TempText()); else if (zone.IsHumidityZone()) - MqttClient.PublishAsync(zone.ToTopic(Topic.current_humidity), zone.TempText(), MqttQualityOfServiceLevel.AtMostOnce, true); + PublishAsync(zone.ToTopic(Topic.current_humidity), zone.TempText()); } private void PublishUnitState(clsUnit unit) { - MqttClient.PublishAsync(unit.ToTopic(Topic.state), unit.ToState(), MqttQualityOfServiceLevel.AtMostOnce, true); + PublishAsync(unit.ToTopic(Topic.state), unit.ToState()); if(unit.Number < 385) - MqttClient.PublishAsync(unit.ToTopic(Topic.brightness_state), unit.ToBrightnessState().ToString(), MqttQualityOfServiceLevel.AtMostOnce, true); + PublishAsync(unit.ToTopic(Topic.brightness_state), unit.ToBrightnessState().ToString()); } private void PublishThermostatState(clsThermostat thermostat) { - MqttClient.PublishAsync(thermostat.ToTopic(Topic.current_operation), thermostat.ToOperationState(), MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync(thermostat.ToTopic(Topic.current_temperature), thermostat.TempText(), MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync(thermostat.ToTopic(Topic.current_humidity), thermostat.HumidityText(), MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync(thermostat.ToTopic(Topic.temperature_heat_state), thermostat.HeatSetpointText(), MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync(thermostat.ToTopic(Topic.temperature_cool_state), thermostat.CoolSetpointText(), MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync(thermostat.ToTopic(Topic.humidify_state), thermostat.HumidifySetpointText(), MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync(thermostat.ToTopic(Topic.dehumidify_state), thermostat.DehumidifySetpointText(), MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync(thermostat.ToTopic(Topic.mode_state), thermostat.ModeText().ToLower(), MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync(thermostat.ToTopic(Topic.fan_mode_state), thermostat.FanModeText().ToLower(), MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync(thermostat.ToTopic(Topic.hold_state), thermostat.HoldStatusText().ToLower(), MqttQualityOfServiceLevel.AtMostOnce, true); + PublishAsync(thermostat.ToTopic(Topic.current_operation), thermostat.ToOperationState()); + PublishAsync(thermostat.ToTopic(Topic.current_temperature), thermostat.TempText()); + PublishAsync(thermostat.ToTopic(Topic.current_humidity), thermostat.HumidityText()); + PublishAsync(thermostat.ToTopic(Topic.temperature_heat_state), thermostat.HeatSetpointText()); + PublishAsync(thermostat.ToTopic(Topic.temperature_cool_state), thermostat.CoolSetpointText()); + PublishAsync(thermostat.ToTopic(Topic.humidify_state), thermostat.HumidifySetpointText()); + PublishAsync(thermostat.ToTopic(Topic.dehumidify_state), thermostat.DehumidifySetpointText()); + PublishAsync(thermostat.ToTopic(Topic.mode_state), thermostat.ModeText().ToLower()); + PublishAsync(thermostat.ToTopic(Topic.fan_mode_state), thermostat.FanModeText().ToLower()); + PublishAsync(thermostat.ToTopic(Topic.hold_state), thermostat.HoldStatusText().ToLower()); + } + + private Task PublishAsync(string topic, string payload) + { + return MqttClient.PublishAsync(topic, payload, MqttQualityOfServiceLevel.AtMostOnce, true); } } } diff --git a/OmniLinkBridge/Modules/OmniLinkII.cs b/OmniLinkBridge/Modules/OmniLinkII.cs index c7410bc..b5914dc 100644 --- a/OmniLinkBridge/Modules/OmniLinkII.cs +++ b/OmniLinkBridge/Modules/OmniLinkII.cs @@ -10,7 +10,7 @@ using System.Threading.Tasks; namespace OmniLinkBridge.Modules { - public class OmniLinkII : IModule + public class OmniLinkII : IModule, IOmniLinkII { private static ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); @@ -39,7 +39,7 @@ namespace OmniLinkBridge.Modules public OmniLinkII(string address, int port, string key1, string key2) { Controller = new clsHAC(); - + Controller.Connection.NetworkAddress = address; Controller.Connection.NetworkPort = (ushort)port; Controller.Connection.ControllerKey = clsUtil.HexString2ByteArray(String.Concat(key1, key2)); @@ -73,6 +73,11 @@ namespace OmniLinkBridge.Modules trigger.Set(); } + public bool SendCommand(enuUnitCommand Cmd, byte Par, ushort Pr2) + { + return Controller.SendCommand(Cmd, Par, Pr2); + } + #region Connection private void Connect() { diff --git a/OmniLinkBridge/OmniLink/IOmniLinkII.cs b/OmniLinkBridge/OmniLink/IOmniLinkII.cs new file mode 100644 index 0000000..ecf1a02 --- /dev/null +++ b/OmniLinkBridge/OmniLink/IOmniLinkII.cs @@ -0,0 +1,11 @@ +using HAI_Shared; + +namespace OmniLinkBridge.OmniLink +{ + public interface IOmniLinkII + { + clsHAC Controller { get; } + + bool SendCommand(enuUnitCommand Cmd, byte Par, ushort Pr2); + } +} diff --git a/OmniLinkBridge/OmniLinkBridge.csproj b/OmniLinkBridge/OmniLinkBridge.csproj index 33d750d..94fdf37 100644 --- a/OmniLinkBridge/OmniLinkBridge.csproj +++ b/OmniLinkBridge/OmniLinkBridge.csproj @@ -77,22 +77,28 @@ + + + - + + + + diff --git a/OmniLinkBridgeTest/MQTTTest.cs b/OmniLinkBridgeTest/MQTTTest.cs new file mode 100644 index 0000000..9cd9ae3 --- /dev/null +++ b/OmniLinkBridgeTest/MQTTTest.cs @@ -0,0 +1,159 @@ +using System; +using System.Text; +using System.Collections.Generic; +using HAI_Shared; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OmniLinkBridge.MQTT; +using OmniLinkBridgeTest.Mock; + +namespace OmniLinkBridgeTest +{ + [TestClass] + public class MQTTTest + { + MockOmniLinkII omniLink; + MessageProcessor messageProcessor; + + [TestInitialize] + public void Initialize() + { + omniLink = new MockOmniLinkII(); + messageProcessor = new MessageProcessor(omniLink); + } + + [TestMethod] + public void AreaCommandInvalid() + { + SendCommandEventArgs actual = null; + omniLink.OnSendCommand += (sender, e) => { actual = e; }; + + // Invalid command + messageProcessor.Process($"omnilink/area1/command", "disarmed"); + Assert.IsNull(actual); + + // Out of range + messageProcessor.Process($"omnilink/area9/command", "disarm"); + Assert.IsNull(actual); + } + + [TestMethod] + public void AreaCommand() + { + void check(ushort id, string payload, enuUnitCommand command) + { + SendCommandEventArgs actual = null; + omniLink.OnSendCommand += (sender, e) => { actual = e; }; + messageProcessor.Process($"omnilink/area{id}/command", payload); + SendCommandEventArgs expected = new SendCommandEventArgs() + { + Cmd = command, + Par = 0, + Pr2 = id + }; + Assert.AreEqual(expected, actual); + } + + // First area standard format + check(1, "disarm", enuUnitCommand.SecurityOff); + check(1, "arm_home", enuUnitCommand.SecurityDay); + check(1, "arm_away", enuUnitCommand.SecurityAway); + check(1, "arm_night", enuUnitCommand.SecurityNight); + check(1, "arm_home_instant", enuUnitCommand.SecurityDyi); + check(1, "arm_night_delay", enuUnitCommand.SecurityNtd); + check(1, "arm_vacation", enuUnitCommand.SecurityVac); + + // Last area with case check + check(8, "DISARM", enuUnitCommand.SecurityOff); + } + + [TestMethod] + public void ZoneCommand() + { + void check(ushort id, string payload, enuUnitCommand command) + { + SendCommandEventArgs actual = null; + omniLink.OnSendCommand += (sender, e) => { actual = e; }; + messageProcessor.Process($"omnilink/zone{id}/command", payload); + SendCommandEventArgs expected = new SendCommandEventArgs() + { + Cmd = command, + Par = 0, + Pr2 = id + }; + Assert.AreEqual(expected, actual); + } + + check(1, "bypass", enuUnitCommand.Bypass); + check(1, "restore", enuUnitCommand.Restore); + + check(2, "BYPASS", enuUnitCommand.Bypass); + } + + [TestMethod] + public void UnitCommand() + { + void check(ushort id, string payload, enuUnitCommand command) + { + SendCommandEventArgs actual = null; + omniLink.OnSendCommand += (sender, e) => { actual = e; }; + messageProcessor.Process($"omnilink/unit{id}/command", payload); + SendCommandEventArgs expected = new SendCommandEventArgs() + { + Cmd = command, + Par = 0, + Pr2 = id + }; + Assert.AreEqual(expected, actual); + } + + check(1, "ON", enuUnitCommand.On); + omniLink.Controller.Units[1].Status = 1; + check(1, "OFF", enuUnitCommand.Off); + + check(2, "on", enuUnitCommand.On); + } + + [TestMethod] + public void UnitLevelCommand() + { + void check(ushort id, string payload, enuUnitCommand command, int level) + { + SendCommandEventArgs actual = null; + omniLink.OnSendCommand += (sender, e) => { actual = e; }; + messageProcessor.Process($"omnilink/unit{id}/brightness_command", payload); + SendCommandEventArgs expected = new SendCommandEventArgs() + { + Cmd = command, + Par = (byte)level, + Pr2 = id + }; + Assert.AreEqual(expected, actual); + } + + check(1, "50", enuUnitCommand.Level, 50); + } + + [TestMethod] + public void ButtonCommand() + { + void check(ushort id, string payload, enuUnitCommand command) + { + SendCommandEventArgs actual = null; + omniLink.OnSendCommand += (sender, e) => { actual = e; }; + messageProcessor.Process($"omnilink/button{id}/command", payload); + SendCommandEventArgs expected = new SendCommandEventArgs() + { + Cmd = command, + Par = 0, + Pr2 = id + }; + Assert.AreEqual(expected, actual); + } + + check(1, "ON", enuUnitCommand.Button); + check(1, "on", enuUnitCommand.Button); + } + } +} + + diff --git a/OmniLinkBridgeTest/Mock/MockOmniLinkII.cs b/OmniLinkBridgeTest/Mock/MockOmniLinkII.cs new file mode 100644 index 0000000..84759ab --- /dev/null +++ b/OmniLinkBridgeTest/Mock/MockOmniLinkII.cs @@ -0,0 +1,30 @@ +using HAI_Shared; +using OmniLinkBridge.OmniLink; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OmniLinkBridgeTest.Mock +{ + class MockOmniLinkII : IOmniLinkII + { + public clsHAC Controller { get; private set; } + + public event EventHandler OnSendCommand; + + public MockOmniLinkII() + { + Controller = new clsHAC(); + Controller.Model = enuModel.OMNI_PRO_II; + Controller.TempFormat = enuTempFormat.Fahrenheit; + } + + public bool SendCommand(enuUnitCommand Cmd, byte Par, ushort Pr2) + { + OnSendCommand?.Invoke(null, new SendCommandEventArgs() { Cmd = Cmd, Par = Par, Pr2 = Pr2 }); + return true; + } + } +} diff --git a/OmniLinkBridgeTest/Mock/SendCommandEventArgs.cs b/OmniLinkBridgeTest/Mock/SendCommandEventArgs.cs new file mode 100644 index 0000000..74fb937 --- /dev/null +++ b/OmniLinkBridgeTest/Mock/SendCommandEventArgs.cs @@ -0,0 +1,33 @@ +using HAI_Shared; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OmniLinkBridgeTest.Mock +{ + public class SendCommandEventArgs : EventArgs + { + public enuUnitCommand Cmd; + public byte Par; + public ushort Pr2; + + public override bool Equals(object other) + { + var toCompareWith = other as SendCommandEventArgs; + + if (toCompareWith == null) + return false; + + return this.Cmd == toCompareWith.Cmd && + this.Par == toCompareWith.Par && + this.Pr2 == toCompareWith.Pr2; + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + } +} diff --git a/OmniLinkBridgeTest/OmniLinkBridgeTest.csproj b/OmniLinkBridgeTest/OmniLinkBridgeTest.csproj index 80e8ca9..8ad0bf6 100644 --- a/OmniLinkBridgeTest/OmniLinkBridgeTest.csproj +++ b/OmniLinkBridgeTest/OmniLinkBridgeTest.csproj @@ -39,12 +39,19 @@ 4 + + False + ..\OmniLinkBridge\HAI.Controller.dll + + + +