From efa16fb2c375a01c3f3a7ed20b3cba4e2cc111c5 Mon Sep 17 00:00:00 2001 From: Ryan Wagoner Date: Thu, 6 Jun 2024 20:20:25 -0400 Subject: [PATCH] 1.1.19 - Fix MQTT alarm control panel for Home Assistant 2024.6 --- OmniLinkBridge/Global.cs | 2 +- OmniLinkBridge/MQTT/Extensions.cs | 13 ++-- OmniLinkBridge/MQTT/HomeAssistant/Alarm.cs | 10 +++ OmniLinkBridge/MQTT/MappingExtensions.cs | 23 +++++- OmniLinkBridge/MQTT/OverrideArea.cs | 19 +++++ OmniLinkBridge/OmniLinkBridge.csproj | 1 + OmniLinkBridge/OmniLinkBridge.ini | 17 ++++- OmniLinkBridge/Properties/AssemblyInfo.cs | 4 +- OmniLinkBridge/Settings.cs | 82 +++++++++++++++++++++- OmniLinkBridgeTest/ExtensionTest.cs | 8 +++ OmniLinkBridgeTest/SettingsTest.cs | 23 +++++- 11 files changed, 184 insertions(+), 18 deletions(-) create mode 100644 OmniLinkBridge/MQTT/OverrideArea.cs diff --git a/OmniLinkBridge/Global.cs b/OmniLinkBridge/Global.cs index 6356fd7..5b3e1c3 100644 --- a/OmniLinkBridge/Global.cs +++ b/OmniLinkBridge/Global.cs @@ -59,7 +59,7 @@ namespace OmniLinkBridge public static string mqtt_discovery_name_prefix; public static HashSet mqtt_discovery_ignore_zones; public static HashSet mqtt_discovery_ignore_units; - public static HashSet mqtt_discovery_area_code_required; + public static ConcurrentDictionary mqtt_discovery_override_area; public static ConcurrentDictionary mqtt_discovery_override_zone; public static ConcurrentDictionary mqtt_discovery_override_unit; public static Type mqtt_discovery_button_type; diff --git a/OmniLinkBridge/MQTT/Extensions.cs b/OmniLinkBridge/MQTT/Extensions.cs index 7dcf2f9..d82df89 100644 --- a/OmniLinkBridge/MQTT/Extensions.cs +++ b/OmniLinkBridge/MQTT/Extensions.cs @@ -1,9 +1,4 @@ using HAI_Shared; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace OmniLinkBridge.MQTT { @@ -28,7 +23,13 @@ namespace OmniLinkBridge.MQTT } else if (supportValidate && payloads.Length == 3) { - if (string.Compare(payloads[1], "validate", true) == 0) + // Special case for Home Assistant when code not required + if (string.Compare(payloads[1], "validate", true) == 0 && + string.Compare(payloads[2], "None", true) == 0) + { + ret.Success = true; + } + else if (string.Compare(payloads[1], "validate", true) == 0) { ret.Validate = true; ret.Success = int.TryParse(payloads[2], out code); diff --git a/OmniLinkBridge/MQTT/HomeAssistant/Alarm.cs b/OmniLinkBridge/MQTT/HomeAssistant/Alarm.cs index 51350fa..169fafb 100644 --- a/OmniLinkBridge/MQTT/HomeAssistant/Alarm.cs +++ b/OmniLinkBridge/MQTT/HomeAssistant/Alarm.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using System.Collections.Generic; namespace OmniLinkBridge.MQTT.HomeAssistant { @@ -16,5 +17,14 @@ namespace OmniLinkBridge.MQTT.HomeAssistant [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public string code { get; set; } + + public bool code_arm_required { get; set; } = false; + + public bool code_disarm_required { get; set; } = false; + + public bool code_trigger_required { get; set; } = false; + + public List supported_features { get; set; } = new List(new string[] { + "arm_home", "arm_away", "arm_night", "arm_vacation" }); } } diff --git a/OmniLinkBridge/MQTT/MappingExtensions.cs b/OmniLinkBridge/MQTT/MappingExtensions.cs index f092bf2..2e00b2c 100644 --- a/OmniLinkBridge/MQTT/MappingExtensions.cs +++ b/OmniLinkBridge/MQTT/MappingExtensions.cs @@ -25,10 +25,27 @@ namespace OmniLinkBridge.MQTT }; - if(Global.mqtt_discovery_area_code_required.Contains(area.Number)) + Global.mqtt_discovery_override_area.TryGetValue(area.Number, out OverrideArea override_area); + + if (override_area != null) { - ret.command_template = "{{ action }},validate,{{ code }}"; - ret.code = "REMOTE_CODE"; + if(override_area.code_arm || override_area.code_disarm) + { + ret.command_template = "{{ action }},validate,{{ code }}"; + ret.code = "REMOTE_CODE"; + } + ret.code_arm_required = override_area.code_arm; + ret.code_disarm_required = override_area.code_disarm; + + ret.supported_features.Clear(); + if (override_area.arm_home) + ret.supported_features.Add("arm_home"); + if (override_area.arm_away) + ret.supported_features.Add("arm_away"); + if (override_area.arm_night) + ret.supported_features.Add("arm_night"); + if (override_area.arm_vacation) + ret.supported_features.Add("arm_vacation"); } return ret; diff --git a/OmniLinkBridge/MQTT/OverrideArea.cs b/OmniLinkBridge/MQTT/OverrideArea.cs new file mode 100644 index 0000000..eee685b --- /dev/null +++ b/OmniLinkBridge/MQTT/OverrideArea.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace OmniLinkBridge.MQTT +{ + public class OverrideArea + { + public bool code_arm { get; set; } + + public bool code_disarm { get; set; } + + public bool arm_home { get; set; } = true; + + public bool arm_away { get; set; } = true; + + public bool arm_night { get; set; } = true; + + public bool arm_vacation { get; set; } = true; + } +} diff --git a/OmniLinkBridge/OmniLinkBridge.csproj b/OmniLinkBridge/OmniLinkBridge.csproj index b53bf53..de236e0 100644 --- a/OmniLinkBridge/OmniLinkBridge.csproj +++ b/OmniLinkBridge/OmniLinkBridge.csproj @@ -87,6 +87,7 @@ + diff --git a/OmniLinkBridge/OmniLinkBridge.ini b/OmniLinkBridge/OmniLinkBridge.ini index 63d893a..1be0988 100644 --- a/OmniLinkBridge/OmniLinkBridge.ini +++ b/OmniLinkBridge/OmniLinkBridge.ini @@ -55,20 +55,31 @@ mqtt_discovery_name_prefix = # Specify a range of numbers 1,2,3,5-10 mqtt_discovery_ignore_zones = mqtt_discovery_ignore_units = -# Require Home Assistant to prompt for user code when arming/disarming area -# Specify a range of numbers 1,2,3,5-10 -mqtt_discovery_area_code_required = + +# Override the area Home Assistant alarm control panel +# Prompt for user code +# code_arm: true or false, defaults to false +# code_disarm: true or false, defaults to false +# Show these modes +# arm_home: true or false, defaults to true +# arm_away: true or false, defaults to true +# arm_night: true or false, defaults to true +# arm_vacation: true or false, defaults to true +#mqtt_discovery_override_area = id=1;code_disarm=true;arm_vacation=false + # Override the zone Home Assistant binary sensor device_class # device_class: must be battery, cold, door, garage_door, gas, # heat, moisture, motion, problem, safety, smoke, or window #mqtt_discovery_override_zone = id=5;device_class=garage_door #mqtt_discovery_override_zone = id=6;device_class=garage_door + # Override the unit Home Assistant device type # type: # Units (LTe 1-32, IIe 1-64, Pro 1-256) light or switch, defaults to light # Flags (LTe 41-88, IIe 73-128, Pro 393-511) switch or number, defaults to switch #mqtt_discovery_override_unit = id=1;type=switch #mqtt_discovery_override_unit = id=395;type=number + # Publish buttons as this Home Assistant device type # must be button (recommended) or switch (default, previous behavior) mqtt_discovery_button_type = switch diff --git a/OmniLinkBridge/Properties/AssemblyInfo.cs b/OmniLinkBridge/Properties/AssemblyInfo.cs index ae19c83..e2d600b 100644 --- a/OmniLinkBridge/Properties/AssemblyInfo.cs +++ b/OmniLinkBridge/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.1.18.0")] -[assembly: AssemblyFileVersion("1.1.18.0")] +[assembly: AssemblyVersion("1.1.19.0")] +[assembly: AssemblyFileVersion("1.1.19.0")] diff --git a/OmniLinkBridge/Settings.cs b/OmniLinkBridge/Settings.cs index c8c161f..3bac7fb 100644 --- a/OmniLinkBridge/Settings.cs +++ b/OmniLinkBridge/Settings.cs @@ -88,7 +88,7 @@ namespace OmniLinkBridge Global.mqtt_discovery_ignore_zones = settings.ValidateRange("mqtt_discovery_ignore_zones"); Global.mqtt_discovery_ignore_units = settings.ValidateRange("mqtt_discovery_ignore_units"); - Global.mqtt_discovery_area_code_required = settings.ValidateRange("mqtt_discovery_area_code_required"); + Global.mqtt_discovery_override_area = settings.LoadOverrideArea("mqtt_discovery_override_area"); Global.mqtt_discovery_override_zone = settings.LoadOverrideZone("mqtt_discovery_override_zone"); Global.mqtt_discovery_override_unit = settings.LoadOverrideUnit("mqtt_discovery_override_unit"); Global.mqtt_discovery_button_type = settings.ValidateType("mqtt_discovery_button_type", typeof(Switch), typeof(Button)); @@ -134,6 +134,86 @@ namespace OmniLinkBridge return value; } + private static ConcurrentDictionary LoadOverrideArea(this NameValueCollection settings, string section) where T : new() + { + try + { + ConcurrentDictionary ret = new ConcurrentDictionary(); + + string value = settings.CheckEnv(section); + + if (string.IsNullOrEmpty(value)) + return ret; + + string[] ids = value.Split(','); + + for (int i = 0; i < ids.Length; i++) + { + Dictionary attributes = ids[i].TrimEnd(new char[] { ';' }).Split(';') + .Select(s => s.Split('=')) + .ToDictionary(a => a[0].Trim(), a => a[1].Trim(), StringComparer.InvariantCultureIgnoreCase); + + if (!attributes.ContainsKey("id") || !int.TryParse(attributes["id"], out int attrib_id)) + throw new Exception("Missing or invalid id attribute"); + + T override_area = new T(); + + if (override_area is MQTT.OverrideArea mqtt_area) + { + foreach (string attribute in attributes.Keys) + { + switch(attribute) + { + case "id": + continue; + case "code_arm": + if (!bool.TryParse(attributes["code_arm"], out bool code_arm)) + throw new Exception("Invalid code_arm attribute"); + mqtt_area.code_arm = code_arm; + break; + case "code_disarm": + if (!bool.TryParse(attributes["code_disarm"], out bool code_disarm)) + throw new Exception("Invalid code_disarm attribute"); + mqtt_area.code_disarm = code_disarm; + break; + case "arm_home": + if (!bool.TryParse(attributes["arm_home"], out bool arm_home)) + throw new Exception("Invalid arm_home attribute"); + mqtt_area.arm_home = arm_home; + break; + case "arm_away": + if (!bool.TryParse(attributes["arm_away"], out bool arm_away)) + throw new Exception("Invalid arm_away attribute"); + mqtt_area.arm_away = arm_away; + break; + case "arm_night": + if (!bool.TryParse(attributes["arm_night"], out bool arm_night)) + throw new Exception("Invalid arm_night attribute"); + mqtt_area.arm_night = arm_night; + break; + case "arm_vacation": + if (!bool.TryParse(attributes["arm_vacation"], out bool arm_vacation)) + throw new Exception("Invalid arm_vacation attribute"); + mqtt_area.arm_vacation = arm_vacation; + break; + default: + throw new Exception($"Unknown attribute {attribute}" ); + } + } + } + + ret.TryAdd(attrib_id, override_area); + } + + return ret; + } + catch (Exception ex) + { + log.Error(ex, "Invalid override area specified for {section}", section); + throw; + } + } + private static ConcurrentDictionary LoadOverrideZone(this NameValueCollection settings, string section) where T : new() { try diff --git a/OmniLinkBridgeTest/ExtensionTest.cs b/OmniLinkBridgeTest/ExtensionTest.cs index 9e5c983..8c45899 100644 --- a/OmniLinkBridgeTest/ExtensionTest.cs +++ b/OmniLinkBridgeTest/ExtensionTest.cs @@ -64,6 +64,14 @@ namespace OmniLinkBridgeTest Assert.AreEqual(parser.Validate, true); Assert.AreEqual(parser.Code, 1234); + // Special case for Home Assistant when code not required + payload = "disarm,validate,None"; + parser = payload.ToCommandCode(supportValidate: true); + Assert.AreEqual(parser.Success, true); + Assert.AreEqual(parser.Command, "disarm"); + Assert.AreEqual(parser.Validate, false); + Assert.AreEqual(parser.Code, 0); + // Falures payload = "disarm,1a"; parser = payload.ToCommandCode(supportValidate: true); diff --git a/OmniLinkBridgeTest/SettingsTest.cs b/OmniLinkBridgeTest/SettingsTest.cs index 43cbff4..67eb3a9 100644 --- a/OmniLinkBridgeTest/SettingsTest.cs +++ b/OmniLinkBridgeTest/SettingsTest.cs @@ -157,7 +157,8 @@ namespace OmniLinkBridgeTest "mqtt_discovery_name_prefix = mynameprefix", "mqtt_discovery_ignore_zones = 1,2-3,4", "mqtt_discovery_ignore_units = 2-5,7", - "mqtt_discovery_area_code_required = 1", + "mqtt_discovery_override_area = id=1", + "mqtt_discovery_override_area = id=2;code_arm=true;code_disarm=true;arm_home=false;arm_away=false;arm_night=false;arm_vacation=false", "mqtt_discovery_override_zone = id=5;device_class=garage_door", "mqtt_discovery_override_zone = id=7;device_class=motion", "mqtt_discovery_override_unit = id=1;type=switch", @@ -171,7 +172,25 @@ namespace OmniLinkBridgeTest Assert.AreEqual("mynameprefix ", Global.mqtt_discovery_name_prefix); Assert.IsTrue(Global.mqtt_discovery_ignore_zones.SetEquals(new int[] { 1, 2, 3, 4 })); Assert.IsTrue(Global.mqtt_discovery_ignore_units.SetEquals(new int[] { 2, 3, 4, 5, 7 })); - Assert.IsTrue(Global.mqtt_discovery_area_code_required.SetEquals(new int[] { 1 })); + + Dictionary override_area = new Dictionary() + { + { 1, new OmniLinkBridge.MQTT.OverrideArea { }}, + { 2, new OmniLinkBridge.MQTT.OverrideArea { code_arm = true, code_disarm = true, + arm_home = false, arm_away = false, arm_night = false, arm_vacation = false }}, + }; + + Assert.AreEqual(override_area.Count, Global.mqtt_discovery_override_area.Count); + foreach (KeyValuePair pair in override_area) + { + Global.mqtt_discovery_override_area.TryGetValue(pair.Key, out OmniLinkBridge.MQTT.OverrideArea value); + Assert.AreEqual(override_area[pair.Key].code_arm, value.code_arm); + Assert.AreEqual(override_area[pair.Key].code_disarm, value.code_disarm); + Assert.AreEqual(override_area[pair.Key].arm_home, value.arm_home); + Assert.AreEqual(override_area[pair.Key].arm_away, value.arm_away); + Assert.AreEqual(override_area[pair.Key].arm_night, value.arm_night); + Assert.AreEqual(override_area[pair.Key].arm_vacation, value.arm_vacation); + } Dictionary override_zone = new Dictionary() {