From 84b52c8f30f58ab2d04924fb3e2052f0469a4256 Mon Sep 17 00:00:00 2001 From: Ryan Wagoner Date: Wed, 10 Apr 2024 20:53:48 -0400 Subject: [PATCH] Add MQTT lock support --- OmniLinkBridge/Global.cs | 1 + OmniLinkBridge/MQTT/HomeAssistant/Lock.cs | 21 ++++++++++ OmniLinkBridge/MQTT/MappingExtensions.cs | 30 +++++++++++++++ OmniLinkBridge/MQTT/MessageProcessor.cs | 21 ++++++++++ OmniLinkBridge/MQTT/Parser/CommandTypes.cs | 3 +- OmniLinkBridge/MQTT/Parser/LockCommands.cs | 8 ++++ OmniLinkBridge/Modules/LoggerModule.cs | 25 +++++++++++- OmniLinkBridge/Modules/MQTTModule.cs | 38 +++++++++++++++++++ OmniLinkBridge/Modules/OmniLinkII.cs | 16 ++++++++ .../OmniLink/LockStatusEventArgs.cs | 11 ++++++ OmniLinkBridge/OmniLinkBridge.csproj | 3 ++ OmniLinkBridge/OmniLinkBridge.ini | 1 + OmniLinkBridge/Properties/AssemblyInfo.cs | 2 +- OmniLinkBridge/Settings.cs | 1 + OmniLinkBridgeTest/MQTTTest.cs | 27 +++++++++++++ OmniLinkBridgeTest/Properties/AssemblyInfo.cs | 2 +- OmniLinkBridgeTest/SettingsTest.cs | 3 +- README.md | 12 ++++++ 18 files changed, 219 insertions(+), 6 deletions(-) create mode 100644 OmniLinkBridge/MQTT/HomeAssistant/Lock.cs create mode 100644 OmniLinkBridge/MQTT/Parser/LockCommands.cs create mode 100644 OmniLinkBridge/OmniLink/LockStatusEventArgs.cs diff --git a/OmniLinkBridge/Global.cs b/OmniLinkBridge/Global.cs index 1136103..1c8ad4c 100644 --- a/OmniLinkBridge/Global.cs +++ b/OmniLinkBridge/Global.cs @@ -31,6 +31,7 @@ namespace OmniLinkBridge public static bool verbose_thermostat; public static bool verbose_unit; public static bool verbose_message; + public static bool verbose_lock; // mySQL Logging public static bool mysql_logging; diff --git a/OmniLinkBridge/MQTT/HomeAssistant/Lock.cs b/OmniLinkBridge/MQTT/HomeAssistant/Lock.cs new file mode 100644 index 0000000..98d2fb6 --- /dev/null +++ b/OmniLinkBridge/MQTT/HomeAssistant/Lock.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; + +namespace OmniLinkBridge.MQTT.HomeAssistant +{ + public class Lock : Device + { + public string command_topic { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string payload_lock { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string payload_unlock { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string state_locked { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string state_unlocked { get; set; } + } +} diff --git a/OmniLinkBridge/MQTT/MappingExtensions.cs b/OmniLinkBridge/MQTT/MappingExtensions.cs index b577d0a..a466bf0 100644 --- a/OmniLinkBridge/MQTT/MappingExtensions.cs +++ b/OmniLinkBridge/MQTT/MappingExtensions.cs @@ -605,5 +605,35 @@ namespace OmniLinkBridge.MQTT else return "off"; } + + public static string ToTopic(this clsAccessControlReader reader, Topic topic) + { + return $"{Global.mqtt_prefix}/lock{reader.Number}/{topic}"; + } + + public static Lock ToConfig(this clsAccessControlReader reader) + { + Lock ret = new Lock + { + unique_id = $"{Global.mqtt_prefix}lock{reader.Number}", + name = Global.mqtt_discovery_name_prefix + reader.Name, + state_topic = reader.ToTopic(Topic.state), + command_topic = reader.ToTopic(Topic.command), + payload_lock = "lock", + payload_unlock = "unlock", + state_locked = "locked", + state_unlocked = "unlocked" + }; + + return ret; + } + + public static string ToState(this clsAccessControlReader reader) + { + if (reader.LockStatus == 0) + return "locked"; + else + return "unlocked"; + } } } diff --git a/OmniLinkBridge/MQTT/MessageProcessor.cs b/OmniLinkBridge/MQTT/MessageProcessor.cs index a1e96e6..e06947f 100644 --- a/OmniLinkBridge/MQTT/MessageProcessor.cs +++ b/OmniLinkBridge/MQTT/MessageProcessor.cs @@ -49,6 +49,8 @@ namespace OmniLinkBridge.MQTT 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); + else if (type == CommandTypes.@lock && id <= OmniLink.Controller.AccessControlReaders.Count) + ProcessLockReceived(OmniLink.Controller.AccessControlReaders[id], topic, payload); } private static readonly IDictionary AreaMapping = new Dictionary @@ -294,5 +296,24 @@ namespace OmniLinkBridge.MQTT OmniLink.SendCommand(MessageMapping[cmd], par, (ushort)message.Number); } } + + private static readonly IDictionary LockMapping = new Dictionary + { + { LockCommands.@lock, enuUnitCommand.Lock }, + { LockCommands.unlock, enuUnitCommand.Unlock }, + }; + + private void ProcessLockReceived(clsAccessControlReader reader, Topic command, string payload) + { + if (command == Topic.command && Enum.TryParse(payload, true, out LockCommands cmd)) + { + if (reader.Number == 0) + log.Debug("SetLock: 0 implies all locks will be changed"); + + log.Debug("SetLock: {id} to {value}", reader.Number, payload); + + OmniLink.SendCommand(LockMapping[cmd], 0, (ushort)reader.Number); + } + } } } diff --git a/OmniLinkBridge/MQTT/Parser/CommandTypes.cs b/OmniLinkBridge/MQTT/Parser/CommandTypes.cs index 99e42f5..ade6e59 100644 --- a/OmniLinkBridge/MQTT/Parser/CommandTypes.cs +++ b/OmniLinkBridge/MQTT/Parser/CommandTypes.cs @@ -7,6 +7,7 @@ unit, thermostat, button, - message + message, + @lock } } diff --git a/OmniLinkBridge/MQTT/Parser/LockCommands.cs b/OmniLinkBridge/MQTT/Parser/LockCommands.cs new file mode 100644 index 0000000..22e6eb1 --- /dev/null +++ b/OmniLinkBridge/MQTT/Parser/LockCommands.cs @@ -0,0 +1,8 @@ +namespace OmniLinkBridge.MQTT.Parser +{ + enum LockCommands + { + @lock, + unlock + } +} diff --git a/OmniLinkBridge/Modules/LoggerModule.cs b/OmniLinkBridge/Modules/LoggerModule.cs index 0580c15..8b2f05f 100644 --- a/OmniLinkBridge/Modules/LoggerModule.cs +++ b/OmniLinkBridge/Modules/LoggerModule.cs @@ -39,6 +39,7 @@ namespace OmniLinkBridge.Modules omnilink.OnThermostatStatus += Omnilink_OnThermostatStatus; omnilink.OnUnitStatus += Omnilink_OnUnitStatus; omnilink.OnMessageStatus += Omnilink_OnMessageStatus; + omnilink.OnLockStatus += Omnilink_OnLockStatus; omnilink.OnSystemStatus += Omnilink_OnSystemStatus; } @@ -199,10 +200,24 @@ namespace OmniLinkBridge.Modules thermostatUsage++; } + ushort lockUsage = 0; + for (ushort i = 1; i <= omnilink.Controller.AccessControlReaders.Count; i++) + { + clsAccessControlReader reader = omnilink.Controller.AccessControlReaders[i]; + + if (reader.DefaultProperties == true) + continue; + + lockUsage++; + + if(Global.verbose_lock) + log.Verbose("Initial LockStatus {id} {name}, Status: {status}", i, reader.Name, reader.LockStatusText()); + } + using (LogContext.PushProperty("Telemetry", "ControllerUsage")) log.Debug("Controller has {AreaUsage} areas, {ZoneUsage} zones, {UnitUsage} units, " + - "{OutputUsage} outputs, {FlagUsage} flags, {ThermostatUsage} thermostats", - areaUsage, zoneUsage, unitUsage, outputUsage, flagUsage, thermostatUsage); + "{OutputUsage} outputs, {FlagUsage} flags, {ThermostatUsage} thermostats, {LockUsage} locks", + areaUsage, zoneUsage, unitUsage, outputUsage, flagUsage, thermostatUsage, lockUsage); } private void Omnilink_OnAreaStatus(object sender, AreaStatusEventArgs e) @@ -337,6 +352,12 @@ namespace OmniLinkBridge.Modules Notification.Notify("Message", e.ID + " " + e.Message.Name + ", " + e.Message.StatusText()); } + private void Omnilink_OnLockStatus(object sender, LockStatusEventArgs e) + { + if (Global.verbose_lock) + log.Verbose("LockStatus {id} {name}, Status: {status}", e.ID, e.Reader.Name, e.Reader.LockStatusText()); + } + private void Omnilink_OnSystemStatus(object sender, SystemStatusEventArgs e) { DBQueue(@" diff --git a/OmniLinkBridge/Modules/MQTTModule.cs b/OmniLinkBridge/Modules/MQTTModule.cs index adaf4ea..3868d60 100644 --- a/OmniLinkBridge/Modules/MQTTModule.cs +++ b/OmniLinkBridge/Modules/MQTTModule.cs @@ -50,6 +50,7 @@ namespace OmniLinkBridge.Modules OmniLink.OnThermostatStatus += Omnilink_OnThermostatStatus; OmniLink.OnButtonStatus += OmniLink_OnButtonStatus; OmniLink.OnMessageStatus += OmniLink_OnMessageStatus; + OmniLink.OnLockStatus += OmniLink_OnLockStatus; OmniLink.OnSystemStatus += OmniLink_OnSystemStatus; MessageProcessor = new MessageProcessor(omni); @@ -169,6 +170,7 @@ namespace OmniLinkBridge.Modules PublishThermostats(); PublishButtons(); PublishMessages(); + PublishLocks(); PublishControllerStatus(ONLINE); PublishAsync($"{Global.mqtt_prefix}/model", OmniLink.Controller.GetModelText()); @@ -429,6 +431,29 @@ namespace OmniLinkBridge.Modules } } + private void PublishLocks() + { + log.Debug("Publishing {type}", "locks"); + + for (ushort i = 1; i <= OmniLink.Controller.AccessControlReaders.Count; i++) + { + clsAccessControlReader reader = OmniLink.Controller.AccessControlReaders[i]; + + if (reader.DefaultProperties == true) + { + PublishAsync(reader.ToTopic(Topic.name), null); + PublishAsync($"{Global.mqtt_discovery_prefix}/lock/{Global.mqtt_prefix}/lock{i}/config", null); + continue; + } + + PublishLockStateAsync(reader); + + PublishAsync(reader.ToTopic(Topic.name), reader.Name); + PublishAsync($"{Global.mqtt_discovery_prefix}/lock/{Global.mqtt_prefix}/lock{i}/config", + JsonConvert.SerializeObject(reader.ToConfig())); + } + } + private void Omnilink_OnAreaStatus(object sender, AreaStatusEventArgs e) { if (!MqttClient.IsConnected) @@ -514,6 +539,14 @@ namespace OmniLinkBridge.Modules PublishMessageStateAsync(e.Message); } + private void OmniLink_OnLockStatus(object sender, LockStatusEventArgs e) + { + if (!MqttClient.IsConnected) + return; + + PublishLockStateAsync(e.Reader); + } + private void OmniLink_OnSystemStatus(object sender, SystemStatusEventArgs e) { if (!MqttClient.IsConnected) @@ -590,6 +623,11 @@ namespace OmniLinkBridge.Modules return PublishAsync(message.ToTopic(Topic.state), message.ToState()); } + private Task PublishLockStateAsync(clsAccessControlReader reader) + { + return PublishAsync(reader.ToTopic(Topic.state), reader.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 2c8a1df..3363402 100644 --- a/OmniLinkBridge/Modules/OmniLinkII.cs +++ b/OmniLinkBridge/Modules/OmniLinkII.cs @@ -40,6 +40,7 @@ namespace OmniLinkBridge.Modules public event EventHandler OnUnitStatus; public event EventHandler OnButtonStatus; public event EventHandler OnMessageStatus; + public event EventHandler OnLockStatus; public event EventHandler OnSystemStatus; private readonly AutoResetEvent trigger = new AutoResetEvent(false); @@ -214,6 +215,7 @@ namespace OmniLinkBridge.Modules await GetNamed(enuObjectType.Unit); await GetNamed(enuObjectType.Message); await GetNamed(enuObjectType.Button); + await GetNamed(enuObjectType.AccessControlReader); } private async Task GetSystemFormats() @@ -342,6 +344,9 @@ namespace OmniLinkBridge.Modules case enuObjectType.Button: Controller.Buttons.CopyProperties(MSG); break; + case enuObjectType.AccessControlReader: + Controller.AccessControlReaders.CopyProperties(MSG); + break; default: break; } @@ -679,6 +684,17 @@ namespace OmniLinkBridge.Modules }); } break; + case enuObjectType.AccessControlLock: + for (byte i = 0; i < MSG.AccessControlLockCount(); i++) + { + Controller.AccessControlReaders[MSG.ObjectNumber(i)].CopyExtendedStatus(MSG, i); + OnLockStatus?.Invoke(this, new LockStatusEventArgs() + { + ID = MSG.ObjectNumber(i), + Reader = Controller.AccessControlReaders[MSG.ObjectNumber(i)] + }); + } + break; default: if (Global.verbose_unhandled) { diff --git a/OmniLinkBridge/OmniLink/LockStatusEventArgs.cs b/OmniLinkBridge/OmniLink/LockStatusEventArgs.cs new file mode 100644 index 0000000..87f40ac --- /dev/null +++ b/OmniLinkBridge/OmniLink/LockStatusEventArgs.cs @@ -0,0 +1,11 @@ +using HAI_Shared; +using System; + +namespace OmniLinkBridge.OmniLink +{ + public class LockStatusEventArgs : EventArgs + { + public ushort ID { get; set; } + public clsAccessControlReader Reader { get; set; } + } +} diff --git a/OmniLinkBridge/OmniLinkBridge.csproj b/OmniLinkBridge/OmniLinkBridge.csproj index 5090558..88e60aa 100644 --- a/OmniLinkBridge/OmniLinkBridge.csproj +++ b/OmniLinkBridge/OmniLinkBridge.csproj @@ -83,6 +83,7 @@ + @@ -95,6 +96,7 @@ + @@ -115,6 +117,7 @@ + diff --git a/OmniLinkBridge/OmniLinkBridge.ini b/OmniLinkBridge/OmniLinkBridge.ini index 817a21e..73e4cd3 100644 --- a/OmniLinkBridge/OmniLinkBridge.ini +++ b/OmniLinkBridge/OmniLinkBridge.ini @@ -22,6 +22,7 @@ verbose_thermostat_timer = yes verbose_thermostat = yes verbose_unit = yes verbose_message = yes +verbose_lock = yes # mySQL Logging (yes/no) mysql_logging = no diff --git a/OmniLinkBridge/Properties/AssemblyInfo.cs b/OmniLinkBridge/Properties/AssemblyInfo.cs index a6dd9c0..8875857 100644 --- a/OmniLinkBridge/Properties/AssemblyInfo.cs +++ b/OmniLinkBridge/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Excalibur Partners, LLC")] [assembly: AssemblyProduct("OmniLinkBridge")] -[assembly: AssemblyCopyright("Copyright © Excalibur Partners, LLC 2022")] +[assembly: AssemblyCopyright("Copyright © Excalibur Partners, LLC 2024")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/OmniLinkBridge/Settings.cs b/OmniLinkBridge/Settings.cs index 622ab3f..8ffa577 100644 --- a/OmniLinkBridge/Settings.cs +++ b/OmniLinkBridge/Settings.cs @@ -52,6 +52,7 @@ namespace OmniLinkBridge Global.verbose_thermostat = settings.ValidateBool("verbose_thermostat"); Global.verbose_unit = settings.ValidateBool("verbose_unit"); Global.verbose_message = settings.ValidateBool("verbose_message"); + Global.verbose_lock = settings.ValidateBool("verbose_lock"); // mySQL Logging Global.mysql_logging = settings.ValidateBool("mysql_logging"); diff --git a/OmniLinkBridgeTest/MQTTTest.cs b/OmniLinkBridgeTest/MQTTTest.cs index 9b997da..1e7a239 100644 --- a/OmniLinkBridgeTest/MQTTTest.cs +++ b/OmniLinkBridgeTest/MQTTTest.cs @@ -286,6 +286,33 @@ namespace OmniLinkBridgeTest check(2, "SHOW", enuUnitCommand.ShowMsgWBeep, 0); } + + [TestMethod] + public void LockCommand() + { + void check(ushort id, string payload, enuUnitCommand command) + { + SendCommandEventArgs actual = null; + omniLink.OnSendCommand += (sender, e) => { actual = e; }; + messageProcessor.Process($"omnilink/lock{id}/command", payload); + SendCommandEventArgs expected = new SendCommandEventArgs() + { + Cmd = command, + Par = 0, + Pr2 = id + }; + Assert.AreEqual(expected, actual); + } + + check(1, "lock", enuUnitCommand.Lock); + check(1, "unlock", enuUnitCommand.Unlock); + + // Check all locks + check(0, "lock", enuUnitCommand.Lock); + + // Check case insensitivity + check(2, "LOCK", enuUnitCommand.Lock); + } } } diff --git a/OmniLinkBridgeTest/Properties/AssemblyInfo.cs b/OmniLinkBridgeTest/Properties/AssemblyInfo.cs index fee5a78..e45d79a 100644 --- a/OmniLinkBridgeTest/Properties/AssemblyInfo.cs +++ b/OmniLinkBridgeTest/Properties/AssemblyInfo.cs @@ -7,7 +7,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Excalibur Partners, LLC")] [assembly: AssemblyProduct("OmniLinkBridgeTest")] -[assembly: AssemblyCopyright("Copyright © Excalibur Partners, LLC 2022")] +[assembly: AssemblyCopyright("Copyright © Excalibur Partners, LLC 2024")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/OmniLinkBridgeTest/SettingsTest.cs b/OmniLinkBridgeTest/SettingsTest.cs index ee73b04..296acd2 100644 --- a/OmniLinkBridgeTest/SettingsTest.cs +++ b/OmniLinkBridgeTest/SettingsTest.cs @@ -78,7 +78,8 @@ namespace OmniLinkBridgeTest "verbose_thermostat_timer", "verbose_thermostat", "verbose_unit", - "verbose_message" + "verbose_message", + "verbose_lock" }) { List lines = new List(RequiredSettings()) diff --git a/README.md b/README.md index 6dd82e8..7b95dc8 100644 --- a/README.md +++ b/README.md @@ -312,6 +312,18 @@ PUB omnilink/messageX/command string show, show_no_beep, show_no_beep_or_led, clear ``` +### Locks +``` +SUB omnilink/lockX/name +string Lock name + +SUB omnilink/lockX/state +string locked, unlocked + +PUB omnilink/lockX/command +string lock, unlock +``` + ## 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.