diff --git a/OmniLinkBridge/MQTT/CommandTypes.cs b/OmniLinkBridge/MQTT/CommandTypes.cs index 657c92c..693aaab 100644 --- a/OmniLinkBridge/MQTT/CommandTypes.cs +++ b/OmniLinkBridge/MQTT/CommandTypes.cs @@ -6,6 +6,7 @@ zone, unit, thermostat, - button + button, + message } } diff --git a/OmniLinkBridge/MQTT/MappingExtensions.cs b/OmniLinkBridge/MQTT/MappingExtensions.cs index 17efa28..6a129fe 100644 --- a/OmniLinkBridge/MQTT/MappingExtensions.cs +++ b/OmniLinkBridge/MQTT/MappingExtensions.cs @@ -140,7 +140,7 @@ namespace OmniLinkBridge.MQTT ret.value_template = "{% if value_json.water_alarm %} ON {%- else -%} OFF {%- endif %}"; return ret; } - + public static BinarySensor ToConfigDuress(this clsArea area) { BinarySensor ret = new BinarySensor(); @@ -468,5 +468,20 @@ namespace OmniLinkBridge.MQTT ret.command_topic = button.ToTopic(Topic.command); return ret; } + + public static string ToTopic(this clsMessage message, Topic topic) + { + return $"{Global.mqtt_prefix}/message{message.Number.ToString()}/{topic.ToString()}"; + } + + public static string ToState(this clsMessage message) + { + if (message.Status == enuMessageStatus.Displayed) + return "displayed"; + else if (message.Status == enuMessageStatus.NotAcked) + return "displayed_not_acknowledged"; + else + return "off"; + } } } diff --git a/OmniLinkBridge/MQTT/MessageCommands.cs b/OmniLinkBridge/MQTT/MessageCommands.cs new file mode 100644 index 0000000..a5c44f8 --- /dev/null +++ b/OmniLinkBridge/MQTT/MessageCommands.cs @@ -0,0 +1,10 @@ +namespace OmniLinkBridge.MQTT +{ + enum MessageCommands + { + show, + show_no_beep, + show_no_beep_or_led, + clear + } +} diff --git a/OmniLinkBridge/MQTT/MessageProcessor.cs b/OmniLinkBridge/MQTT/MessageProcessor.cs index 3e9e7a3..ae3b086 100644 --- a/OmniLinkBridge/MQTT/MessageProcessor.cs +++ b/OmniLinkBridge/MQTT/MessageProcessor.cs @@ -10,7 +10,7 @@ namespace OmniLinkBridge.MQTT { public class MessageProcessor { - private static ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + private static readonly ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); private readonly Regex regexTopic = new Regex(Global.mqtt_prefix + "/([A-Za-z]+)([0-9]+)/(.*)", RegexOptions.Compiled); @@ -45,9 +45,11 @@ namespace OmniLinkBridge.MQTT 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); + else if (type == CommandTypes.message && id > 0 && id <= OmniLink.Controller.Messages.Count) + ProcessMessageReceived(OmniLink.Controller.Messages[id], topic, payload); } - private IDictionary AreaMapping = new Dictionary + private static readonly IDictionary AreaMapping = new Dictionary { { AreaCommands.disarm, enuUnitCommand.SecurityOff }, { AreaCommands.arm_home, enuUnitCommand.SecurityDay }, @@ -71,7 +73,7 @@ namespace OmniLinkBridge.MQTT } } - private IDictionary ZoneMapping = new Dictionary + private static readonly IDictionary ZoneMapping = new Dictionary { { ZoneCommands.restore, enuUnitCommand.Restore }, { ZoneCommands.bypass, enuUnitCommand.Bypass }, @@ -86,7 +88,7 @@ namespace OmniLinkBridge.MQTT } } - private IDictionary UnitMapping = new Dictionary + private static readonly IDictionary UnitMapping = new Dictionary { { UnitCommands.OFF, enuUnitCommand.Off }, { UnitCommands.ON, enuUnitCommand.On } @@ -181,5 +183,29 @@ namespace OmniLinkBridge.MQTT OmniLink.SendCommand(enuUnitCommand.Button, 0, (ushort)button.Number); } } + + private static readonly IDictionary MessageMapping = new Dictionary + { + { MessageCommands.show, enuUnitCommand.ShowMsgWBeep }, + { MessageCommands.show_no_beep, enuUnitCommand.ShowMsgNoBeep }, + { MessageCommands.show_no_beep_or_led, enuUnitCommand.ShowMsgNoBeep }, + { MessageCommands.clear, enuUnitCommand.ClearMsg }, + }; + + private void ProcessMessageReceived(clsMessage message, Topic command, string payload) + { + if (command == Topic.command && Enum.TryParse(payload, true, out MessageCommands cmd)) + { + log.Debug("SetMessage: " + message.Number + " to " + cmd.ToString().Replace("_", " ")); + + byte par = 0; + if (cmd == MessageCommands.show_no_beep) + par = 1; + else if (cmd == MessageCommands.show_no_beep_or_led) + par = 2; + + OmniLink.SendCommand(MessageMapping[cmd], par, (ushort)message.Number); + } + } } } diff --git a/OmniLinkBridge/MQTT/Topic.cs b/OmniLinkBridge/MQTT/Topic.cs index 10c5d93..3aa6112 100644 --- a/OmniLinkBridge/MQTT/Topic.cs +++ b/OmniLinkBridge/MQTT/Topic.cs @@ -2,6 +2,7 @@ { public enum Topic { + name, state, command, basic_state, diff --git a/OmniLinkBridge/Modules/LoggerModule.cs b/OmniLinkBridge/Modules/LoggerModule.cs index 7dbac55..e672266 100644 --- a/OmniLinkBridge/Modules/LoggerModule.cs +++ b/OmniLinkBridge/Modules/LoggerModule.cs @@ -276,7 +276,7 @@ namespace OmniLinkBridge.Modules VALUES ('" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "','" + e.ID + "','" + e.Message.Name + "','" + e.Message.StatusText() + "')"); if (Global.verbose_message) - log.Debug("MessageStatus " + e.Message.Name + ", " + e.Message.StatusText()); + log.Debug("MessageStatus " + e.ID + " " + e.Message.Name + ", " + e.Message.StatusText()); if (Global.notify_message) Notification.Notify("Message", e.ID + " " + e.Message.Name + ", " + e.Message.StatusText()); diff --git a/OmniLinkBridge/Modules/MQTTModule.cs b/OmniLinkBridge/Modules/MQTTModule.cs index 0402a73..258cdab 100644 --- a/OmniLinkBridge/Modules/MQTTModule.cs +++ b/OmniLinkBridge/Modules/MQTTModule.cs @@ -2,6 +2,10 @@ using log4net; using MQTTnet; using MQTTnet.Client; +using MQTTnet.Client.Connecting; +using MQTTnet.Client.Disconnecting; +using MQTTnet.Client.Options; +using MQTTnet.Client.Receiving; using MQTTnet.Extensions.ManagedClient; using MQTTnet.Protocol; using Newtonsoft.Json; @@ -38,6 +42,8 @@ namespace OmniLinkBridge.Modules OmniLink.OnZoneStatus += Omnilink_OnZoneStatus; OmniLink.OnUnitStatus += Omnilink_OnUnitStatus; OmniLink.OnThermostatStatus += Omnilink_OnThermostatStatus; + OmniLink.OnButtonStatus += OmniLink_OnButtonStatus; + OmniLink.OnMessageStatus += OmniLink_OnMessageStatus; MessageProcessor = new MessageProcessor(omni); } @@ -66,7 +72,7 @@ namespace OmniLinkBridge.Modules .Build(); MqttClient = new MqttFactory().CreateManagedMqttClient(); - MqttClient.Connected += (sender, e) => + MqttClient.ConnectedHandler = new MqttClientConnectedHandlerDelegate((e) => { log.Debug("Connected"); @@ -83,13 +89,14 @@ namespace OmniLinkBridge.Modules // For subsequent connections publish config immediately if (ControllerConnected) PublishConfig(); - }; - MqttClient.ConnectingFailed += (sender, e) => { log.Debug("Error connecting " + e.Exception.Message); }; - MqttClient.Disconnected += (sender, e) => { log.Debug("Disconnected"); }; + }); + MqttClient.ConnectingFailedHandler = new ConnectingFailedHandlerDelegate((e) => log.Debug("Error connecting " + e.Exception.Message)); + MqttClient.DisconnectedHandler = new MqttClientDisconnectedHandlerDelegate((e) => log.Debug("Disconnected")); MqttClient.StartAsync(manoptions); - MqttClient.ApplicationMessageReceived += MqttClient_ApplicationMessageReceived; + MqttClient.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate((e) => + MessageProcessor.Process(e.ApplicationMessage.Topic, Encoding.UTF8.GetString(e.ApplicationMessage.Payload))); // Subscribe to notifications for these command topics List toSubscribe = new List() @@ -117,11 +124,6 @@ namespace OmniLinkBridge.Modules MqttClient.StopAsync(); } - private void MqttClient_ApplicationMessageReceived(object sender, MqttApplicationMessageReceivedEventArgs e) - { - MessageProcessor.Process(e.ApplicationMessage.Topic, Encoding.UTF8.GetString(e.ApplicationMessage.Payload)); - } - public void Shutdown() { trigger.Set(); @@ -153,6 +155,7 @@ namespace OmniLinkBridge.Modules PublishUnits(); PublishThermostats(); PublishButtons(); + PublishMessages(); log.Debug("Publishing controller online"); PublishAsync($"{Global.mqtt_prefix}/status", "online"); @@ -172,6 +175,7 @@ namespace OmniLinkBridge.Modules // (configured for 1 area). To workaround ignore default properties for the first area. if (i > 1 && area.DefaultProperties == true) { + PublishAsync(area.ToTopic(Topic.name), null); 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); @@ -186,6 +190,7 @@ namespace OmniLinkBridge.Modules PublishAreaState(area); + PublishAsync(area.ToTopic(Topic.name), area.Name); 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", @@ -215,8 +220,18 @@ namespace OmniLinkBridge.Modules { clsZone zone = OmniLink.Controller.Zones[i]; - if (zone.DefaultProperties == true || Global.mqtt_discovery_ignore_zones.Contains(zone.Number)) + if (zone.DefaultProperties == true) { + PublishAsync(zone.ToTopic(Topic.name), null); + } + else + { + PublishZoneState(zone); + PublishAsync(zone.ToTopic(Topic.name), zone.Name); + } + + if (zone.DefaultProperties == true || Global.mqtt_discovery_ignore_zones.Contains(zone.Number)) + { 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); @@ -224,8 +239,6 @@ namespace OmniLinkBridge.Modules continue; } - PublishZoneState(zone); - 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", @@ -247,7 +260,17 @@ namespace OmniLinkBridge.Modules for (ushort i = 1; i <= OmniLink.Controller.Units.Count; i++) { clsUnit unit = OmniLink.Controller.Units[i]; - + + if (unit.DefaultProperties == true) + { + PublishAsync(unit.ToTopic(Topic.name), null); + } + else + { + PublishUnitState(unit); + PublishAsync(unit.ToTopic(Topic.name), unit.Name); + } + if (unit.DefaultProperties == true || Global.mqtt_discovery_ignore_units.Contains(unit.Number)) { string type = i < 385 ? "light" : "switch"; @@ -255,9 +278,7 @@ namespace OmniLinkBridge.Modules continue; } - PublishUnitState(unit); - - if(i < 385) + if (i < 385) PublishAsync($"{Global.mqtt_discovery_prefix}/light/{Global.mqtt_prefix}/unit{i.ToString()}/config", JsonConvert.SerializeObject(unit.ToConfig())); else @@ -276,6 +297,7 @@ namespace OmniLinkBridge.Modules if (thermostat.DefaultProperties == true) { + PublishAsync(thermostat.ToTopic(Topic.name), null); 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); @@ -284,6 +306,7 @@ namespace OmniLinkBridge.Modules PublishThermostatState(thermostat); + PublishAsync(thermostat.ToTopic(Topic.name), thermostat.Name); 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", @@ -303,18 +326,40 @@ namespace OmniLinkBridge.Modules if (button.DefaultProperties == true) { + PublishAsync(button.ToTopic(Topic.name), null); PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/button{i.ToString()}/config", null); continue; } - // Buttons are always off + // Buttons are off unless momentarily pressed PublishAsync(button.ToTopic(Topic.state), "OFF"); + PublishAsync(button.ToTopic(Topic.name), button.Name); PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/button{i.ToString()}/config", JsonConvert.SerializeObject(button.ToConfig())); } } + private void PublishMessages() + { + log.Debug("Publishing messages"); + + for (ushort i = 1; i <= OmniLink.Controller.Messages.Count; i++) + { + clsMessage message = OmniLink.Controller.Messages[i]; + + if (message.DefaultProperties == true) + { + PublishAsync(message.ToTopic(Topic.name), null); + continue; + } + + PublishMessageState(message); + + PublishAsync(message.ToTopic(Topic.name), message.Name); + } + } + private void Omnilink_OnAreaStatus(object sender, AreaStatusEventArgs e) { if (!MqttClient.IsConnected) @@ -375,6 +420,22 @@ namespace OmniLinkBridge.Modules PublishThermostatState(e.Thermostat); } + private async void OmniLink_OnButtonStatus(object sender, ButtonStatusEventArgs e) + { + if (!MqttClient.IsConnected) + return; + + await PublishButtonState(e.Button); + } + + private void OmniLink_OnMessageStatus(object sender, MessageStatusEventArgs e) + { + if (!MqttClient.IsConnected) + return; + + PublishMessageState(e.Message); + } + private void PublishAreaState(clsArea area) { PublishAsync(area.ToTopic(Topic.state), area.ToState()); @@ -415,6 +476,19 @@ namespace OmniLinkBridge.Modules PublishAsync(thermostat.ToTopic(Topic.hold_state), thermostat.HoldStatusText().ToLower()); } + private async Task PublishButtonState(clsButton button) + { + // Simulate a momentary press + await PublishAsync(button.ToTopic(Topic.state), "ON"); + await Task.Delay(1000); + await PublishAsync(button.ToTopic(Topic.state), "OFF"); + } + + private void PublishMessageState(clsMessage message) + { + PublishAsync(message.ToTopic(Topic.state), message.ToState()); + } + 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 60ae8d5..5361e2a 100644 --- a/OmniLinkBridge/Modules/OmniLinkII.cs +++ b/OmniLinkBridge/Modules/OmniLinkII.cs @@ -30,6 +30,7 @@ namespace OmniLinkBridge.Modules public event EventHandler OnZoneStatus; public event EventHandler OnThermostatStatus; public event EventHandler OnUnitStatus; + public event EventHandler OnButtonStatus; public event EventHandler OnMessageStatus; public event EventHandler OnSystemStatus; @@ -567,6 +568,12 @@ namespace OmniLinkBridge.Modules eventargs.Value = ((int)MSG.SystemEvent).ToString() + " " + Controller.Buttons[MSG.SystemEvent].Name; OnSystemStatus?.Invoke(this, eventargs); + + OnButtonStatus?.Invoke(this, new ButtonStatusEventArgs() + { + ID = MSG.SystemEvent, + Button = Controller.Buttons[MSG.SystemEvent] + }); } else if (MSG.SystemEvent >= 768 && MSG.SystemEvent <= 771) { diff --git a/OmniLinkBridge/OmniLink/ButtonStatusEventArgs.cs b/OmniLinkBridge/OmniLink/ButtonStatusEventArgs.cs new file mode 100644 index 0000000..39f166c --- /dev/null +++ b/OmniLinkBridge/OmniLink/ButtonStatusEventArgs.cs @@ -0,0 +1,11 @@ +using HAI_Shared; +using System; + +namespace OmniLinkBridge.OmniLink +{ + public class ButtonStatusEventArgs : EventArgs + { + public ushort ID { get; set; } + public clsButton Button { get; set; } + } +} diff --git a/OmniLinkBridge/OmniLinkBridge.csproj b/OmniLinkBridge/OmniLinkBridge.csproj index 94fdf37..a96f30b 100644 --- a/OmniLinkBridge/OmniLinkBridge.csproj +++ b/OmniLinkBridge/OmniLinkBridge.csproj @@ -84,6 +84,7 @@ + @@ -98,6 +99,7 @@ + @@ -166,10 +168,10 @@ 4.5.0 - 2.8.4 + 3.0.8 - 11.0.2 + 12.0.3 diff --git a/OmniLinkBridgeTest/MQTTTest.cs b/OmniLinkBridgeTest/MQTTTest.cs index 9cd9ae3..f22bebe 100644 --- a/OmniLinkBridgeTest/MQTTTest.cs +++ b/OmniLinkBridgeTest/MQTTTest.cs @@ -153,6 +153,31 @@ namespace OmniLinkBridgeTest check(1, "ON", enuUnitCommand.Button); check(1, "on", enuUnitCommand.Button); } + + [TestMethod] + public void MessageCommand() + { + void check(ushort id, string payload, enuUnitCommand command, byte par) + { + SendCommandEventArgs actual = null; + omniLink.OnSendCommand += (sender, e) => { actual = e; }; + messageProcessor.Process($"omnilink/message{id}/command", payload); + SendCommandEventArgs expected = new SendCommandEventArgs() + { + Cmd = command, + Par = par, + Pr2 = id + }; + Assert.AreEqual(expected, actual); + } + + check(1, "show", enuUnitCommand.ShowMsgWBeep, 0); + check(1, "show_no_beep", enuUnitCommand.ShowMsgNoBeep, 1); + check(1, "show_no_beep_or_led", enuUnitCommand.ShowMsgNoBeep, 2); + check(1, "clear", enuUnitCommand.ClearMsg, 0); + + check(2, "SHOW", enuUnitCommand.ShowMsgWBeep, 0); + } } } diff --git a/OmniLinkBridgeTest/OmniLinkBridgeTest.csproj b/OmniLinkBridgeTest/OmniLinkBridgeTest.csproj index 8ad0bf6..60e954d 100644 --- a/OmniLinkBridgeTest/OmniLinkBridgeTest.csproj +++ b/OmniLinkBridgeTest/OmniLinkBridgeTest.csproj @@ -57,10 +57,10 @@ - 1.3.2 + 2.0.0 - 1.3.2 + 2.0.0 diff --git a/README.md b/README.md index 4260728..bce3b56 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,9 @@ systemctl start omnilinkbridge.service ### Areas ``` +SUB omnilink/areaX/name +string Area name + SUB omnilink/areaX/state string triggered, pending, armed_night, armed_night_delay, armed_home, armed_home_instant, armed_away, armed_vacation, disarmed @@ -144,6 +147,9 @@ string(insensitive) arm_home, arm_away, arm_night, disarm, arm_home_instant, arm ### Zones ``` +SUB omnilink/zoneX/name +string Zone name + SUB omnilink/zoneX/state string secure, not_ready, trouble, armed, tripped, bypassed @@ -162,6 +168,9 @@ string(insensitive) bypass, restore ### Units ``` +SUB omnilink/unitX/name +string Unit name + SUB omnilink/unitX/state PUB omnilink/unitX/command string OFF, ON @@ -173,6 +182,9 @@ int Level from 0 to 100 percent ### Thermostats ``` +SUB omnilink/thermostatX/name +string Thermostat name + SUB omnilink/thermostatX/current_operation string idle, cool, heat @@ -209,13 +221,29 @@ string off, hold ### Buttons ``` +SUB omnilink/buttonX/name +string Button name + SUB omnilink/buttonX/state -string OFF +string OFF, ON PUB omnilink/buttonX/command string ON ``` +### Messages +``` +SUB omnilink/messageX/name +string Message name + +SUB omnilink/messageX/state +string off, displayed, displayed_not_acknowledged + +PUB omnilink/messageX/command +string show, show_no_beep, show_no_beep_or_led, clear +``` + + ## Web API To test the web service API you can use your browser to view a page or PowerShell (see below) to change a value.