diff --git a/Dockerfile b/Dockerfile index 178629d..8bc2a29 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,7 @@ FROM mono:latest AS build ARG TARGETPLATFORM +ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64} RUN apt-get update && \ apt-get install -y unixodbc diff --git a/OmniLinkBridge/Extensions.cs b/OmniLinkBridge/Extensions.cs index a6af11e..ff7453c 100644 --- a/OmniLinkBridge/Extensions.cs +++ b/OmniLinkBridge/Extensions.cs @@ -1,4 +1,5 @@ -using System; +using OmniLinkBridge.MQTT; +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -24,15 +25,36 @@ namespace OmniLinkBridge return (b & (1 << pos)) != 0; } - public static (string, int) ToCommandCode(this string payload) + public static AreaCommandCode ToCommandCode(this string payload, bool supportValidate = false) { string[] payloads = payload.Split(','); - int code = 0; - if (payloads.Length > 1) - int.TryParse(payloads[1], out code); - return (payloads[0], code); + AreaCommandCode ret = new AreaCommandCode() + { + Command = payloads[0] + }; + + if (payload.Length == 1) + return ret; + + if (payloads.Length == 2) + { + ret.Success = int.TryParse(payloads[1], out code); + } + else if (supportValidate && payloads.Length == 3) + { + if (string.Compare(payloads[1], "validate", true) == 0) + { + ret.Validate = true; + ret.Success = int.TryParse(payloads[2], out code); + } + else + ret.Success = false; + } + + ret.Code = code; + return ret; } public static string ToSpaceTitleCase(this string phrase) diff --git a/OmniLinkBridge/Global.cs b/OmniLinkBridge/Global.cs index a6c7312..b52d525 100644 --- a/OmniLinkBridge/Global.cs +++ b/OmniLinkBridge/Global.cs @@ -53,6 +53,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_zone; // Notifications diff --git a/OmniLinkBridge/MQTT/Alarm.cs b/OmniLinkBridge/MQTT/Alarm.cs index 88e94aa..9d2a9ef 100644 --- a/OmniLinkBridge/MQTT/Alarm.cs +++ b/OmniLinkBridge/MQTT/Alarm.cs @@ -1,9 +1,16 @@ -namespace OmniLinkBridge.MQTT +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace OmniLinkBridge.MQTT { public class Alarm : Device { public string command_topic { get; set; } - //public string code { get; set; } = string.Empty; + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string command_template { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string code { get; set; } } } diff --git a/OmniLinkBridge/MQTT/AreaCommandCode.cs b/OmniLinkBridge/MQTT/AreaCommandCode.cs new file mode 100644 index 0000000..f0ebaa9 --- /dev/null +++ b/OmniLinkBridge/MQTT/AreaCommandCode.cs @@ -0,0 +1,10 @@ +namespace OmniLinkBridge.MQTT +{ + public class AreaCommandCode + { + public bool Success { get; set; } = true; + public string Command { get; set; } + public bool Validate { get; set; } + public int Code { get; set; } + } +} diff --git a/OmniLinkBridge/MQTT/MappingExtensions.cs b/OmniLinkBridge/MQTT/MappingExtensions.cs index 45545a0..5db0507 100644 --- a/OmniLinkBridge/MQTT/MappingExtensions.cs +++ b/OmniLinkBridge/MQTT/MappingExtensions.cs @@ -18,8 +18,16 @@ namespace OmniLinkBridge.MQTT unique_id = $"{Global.mqtt_prefix}area{area.Number}", name = Global.mqtt_discovery_name_prefix + area.Name, state_topic = area.ToTopic(Topic.basic_state), - command_topic = area.ToTopic(Topic.command) + command_topic = area.ToTopic(Topic.command), + }; + + if(Global.mqtt_discovery_area_code_required.Contains(area.Number)) + { + ret.command_template = "{{ action }},validate,{{ code }}"; + ret.code = "REMOTE_CODE"; + } + return ret; } @@ -118,7 +126,7 @@ namespace OmniLinkBridge.MQTT name = $"{Global.mqtt_discovery_name_prefix}{area.Name} Auxiliary", device_class = BinarySensor.DeviceClass.problem, state_topic = area.ToTopic(Topic.json_state), - value_template = "{% if value_json.burglary_alarm %} ON {%- else -%} OFF {%- endif %}" + value_template = "{% if value_json.burglary_alarm %} ON {%- else -%} OFF {%- endif %}" }; return ret; } @@ -177,7 +185,7 @@ namespace OmniLinkBridge.MQTT public static string ToJsonState(this clsArea area) { - AreaState state = new AreaState() + AreaState state = new AreaState { arming = area.ExitTimer > 0, burglary_alarm = area.AreaAlarms.IsBitSet(0), @@ -187,18 +195,17 @@ namespace OmniLinkBridge.MQTT freeze_alarm = area.AreaAlarms.IsBitSet(4), water_alarm = area.AreaAlarms.IsBitSet(5), duress_alarm = area.AreaAlarms.IsBitSet(6), - temperature_alarm = area.AreaAlarms.IsBitSet(7) - }; - - state.mode = area.AreaMode switch - { - enuSecurityMode.Night => "night", - enuSecurityMode.NightDly => "night_delay", - enuSecurityMode.Day => "home", - enuSecurityMode.DayInst => "home_instant", - enuSecurityMode.Away => "away", - enuSecurityMode.Vacation => "vacation", - _ => "off", + temperature_alarm = area.AreaAlarms.IsBitSet(7), + mode = area.AreaMode switch + { + enuSecurityMode.Night => "night", + enuSecurityMode.NightDly => "night_delay", + enuSecurityMode.Day => "home", + enuSecurityMode.DayInst => "home_instant", + enuSecurityMode.Away => "away", + enuSecurityMode.Vacation => "vacation", + _ => "off", + } }; return JsonConvert.SerializeObject(state); } diff --git a/OmniLinkBridge/MQTT/MessageProcessor.cs b/OmniLinkBridge/MQTT/MessageProcessor.cs index 4c1e950..519dac8 100644 --- a/OmniLinkBridge/MQTT/MessageProcessor.cs +++ b/OmniLinkBridge/MQTT/MessageProcessor.cs @@ -64,20 +64,63 @@ namespace OmniLinkBridge.MQTT private void ProcessAreaReceived(clsArea area, Topic command, string payload) { - int code; - (payload, code) = payload.ToCommandCode(); + AreaCommandCode parser = payload.ToCommandCode(supportValidate: true); - if (command == Topic.command && Enum.TryParse(payload, true, out AreaCommands cmd)) + if (parser.Success && command == Topic.command && Enum.TryParse(parser.Command, true, out AreaCommands cmd)) { if (area.Number == 0) log.Debug("SetArea: 0 implies all areas will be changed"); - log.Debug("SetArea: {id} to {value}", area.Number, cmd.ToString().Replace("arm_", "").Replace("_", " ")); - OmniLink.SendCommand(AreaMapping[cmd], (byte)code, (ushort)area.Number); + if (parser.Validate) + { + string sCode = parser.Code.ToString(); + + if(sCode.Length != 4) + { + log.Warning("SetArea: {id}, Invalid security code: must be 4 digits", area.Number); + return; + } + + OmniLink.Controller.Connection.Send(new clsOL2MsgRequestValidateCode(OmniLink.Controller.Connection) + { + Area = (byte)area.Number, + Digit1 = (byte)int.Parse(sCode[0].ToString()), + Digit2 = (byte)int.Parse(sCode[1].ToString()), + Digit3 = (byte)int.Parse(sCode[2].ToString()), + Digit4 = (byte)int.Parse(sCode[3].ToString()) + }, (M, B, Timeout) => + { + if (Timeout || !((B.Length > 3) && (B[0] == 0x21) && (enuOmniLink2MessageType)B[2] == enuOmniLink2MessageType.ValidateCode)) + return; + + var validateCode = new clsOL2MsgValidateCode(OmniLink.Controller.Connection, B); + + if(validateCode.AuthorityLevel == 0) + { + log.Warning("SetArea: {id}, Invalid security code: validation failed", area.Number); + return; + } + + log.Debug("SetArea: {id}, Validated security code, Code Number: {code}, Authority: {authority}", + area.Number, validateCode.CodeNumber, validateCode.AuthorityLevel.ToString()); + + log.Debug("SetArea: {id} to {value}, Code Number: {code}", + area.Number, cmd.ToString().Replace("arm_", "").Replace("_", " "), validateCode.CodeNumber); + + OmniLink.SendCommand(AreaMapping[cmd], validateCode.CodeNumber, (ushort)area.Number); + }); + + return; + } + + log.Debug("SetArea: {id} to {value}, Code Number: {code}", + area.Number, cmd.ToString().Replace("arm_", "").Replace("_", " "), parser.Code); + + OmniLink.SendCommand(AreaMapping[cmd], (byte)parser.Code, (ushort)area.Number); } - else if (command == Topic.alarm_command && area.Number > 0 && Enum.TryParse(payload, true, out AlarmCommands alarm)) + else if (command == Topic.alarm_command && area.Number > 0 && Enum.TryParse(parser.Command, true, out AlarmCommands alarm)) { - log.Debug("SetAreaAlarm: {id} to {value}", area.Number, payload); + log.Debug("SetAreaAlarm: {id} to {value}", area.Number, parser.Command); OmniLink.Controller.Connection.Send(new clsOL2MsgActivateKeypadEmg(OmniLink.Controller.Connection) { @@ -95,17 +138,16 @@ namespace OmniLinkBridge.MQTT private void ProcessZoneReceived(clsZone zone, Topic command, string payload) { - int code; - (payload, code) = payload.ToCommandCode(); + AreaCommandCode parser = payload.ToCommandCode(); - if (command == Topic.command && Enum.TryParse(payload, true, out ZoneCommands cmd) && + if (parser.Success && command == Topic.command && Enum.TryParse(parser.Command, true, out ZoneCommands cmd) && !(zone.Number == 0 && cmd == ZoneCommands.bypass)) { if (zone.Number == 0) log.Debug("SetZone: 0 implies all zones will be restored"); - log.Debug("SetZone: {id} to {value}", zone.Number, payload); - OmniLink.SendCommand(ZoneMapping[cmd], (byte)code, (ushort)zone.Number); + log.Debug("SetZone: {id} to {value}", zone.Number, parser.Command); + OmniLink.SendCommand(ZoneMapping[cmd], (byte)parser.Code, (ushort)zone.Number); } } diff --git a/OmniLinkBridge/OmniLinkBridge.csproj b/OmniLinkBridge/OmniLinkBridge.csproj index 69bd256..cbc9ac5 100644 --- a/OmniLinkBridge/OmniLinkBridge.csproj +++ b/OmniLinkBridge/OmniLinkBridge.csproj @@ -84,6 +84,7 @@ + @@ -174,13 +175,13 @@ 4.5.0 - 3.1.1 + 3.1.2 13.0.1 - 2.10.0 + 2.12.0 1.1.0 @@ -189,7 +190,7 @@ 1.5.0 - 4.0.1 + 4.1.0 5.0.0 diff --git a/OmniLinkBridge/OmniLinkBridge.ini b/OmniLinkBridge/OmniLinkBridge.ini index dcc63eb..aa0b157 100644 --- a/OmniLinkBridge/OmniLinkBridge.ini +++ b/OmniLinkBridge/OmniLinkBridge.ini @@ -52,6 +52,7 @@ mqtt_discovery_name_prefix = # Specify a range of numbers like 1,2,3,5-10 mqtt_discovery_ignore_zones = mqtt_discovery_ignore_units = +mqtt_discovery_area_code_required = # device_class must be battery, door, garage_door, gas, moisture, motion, problem, smoke, or window #mqtt_discovery_override_zone = id=5;device_class=garage_door #mqtt_discovery_override_zone = id=6;device_class=garage_door diff --git a/OmniLinkBridge/Properties/AssemblyInfo.cs b/OmniLinkBridge/Properties/AssemblyInfo.cs index 2545a62..cd82cb7 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.12.0")] -[assembly: AssemblyFileVersion("1.1.12.0")] +[assembly: AssemblyVersion("1.1.13.0")] +[assembly: AssemblyFileVersion("1.1.13.0")] diff --git a/OmniLinkBridge/Settings.cs b/OmniLinkBridge/Settings.cs index f056418..881777b 100644 --- a/OmniLinkBridge/Settings.cs +++ b/OmniLinkBridge/Settings.cs @@ -84,6 +84,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_zone = settings.LoadOverrideZone("mqtt_discovery_override_zone"); } diff --git a/OmniLinkBridgeTest/ExtensionTest.cs b/OmniLinkBridgeTest/ExtensionTest.cs index 8e5f78d..7be36db 100644 --- a/OmniLinkBridgeTest/ExtensionTest.cs +++ b/OmniLinkBridgeTest/ExtensionTest.cs @@ -3,6 +3,7 @@ using System.Text; using System.Collections.Generic; using Microsoft.VisualStudio.TestTools.UnitTesting; using OmniLinkBridge; +using OmniLinkBridge.MQTT; namespace OmniLinkBridgeTest { @@ -41,18 +42,51 @@ namespace OmniLinkBridgeTest [TestMethod] public void TestToCommandCode() { - string payload, command; - int code; + string payload; + AreaCommandCode parser; payload = "disarm"; - (command, code) = payload.ToCommandCode(); - Assert.AreEqual(command, "disarm"); - Assert.AreEqual(code, 0); + 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); payload = "disarm,1"; - (command, code) = payload.ToCommandCode(); - Assert.AreEqual(command, "disarm"); - Assert.AreEqual(code, 1); + parser = payload.ToCommandCode(supportValidate: true); + Assert.AreEqual(parser.Success, true); + Assert.AreEqual(parser.Command, "disarm"); + Assert.AreEqual(parser.Validate, false); + Assert.AreEqual(parser.Code, 1); + + payload = "disarm,validate,1234"; + parser = payload.ToCommandCode(supportValidate: true); + Assert.AreEqual(parser.Success, true); + Assert.AreEqual(parser.Command, "disarm"); + Assert.AreEqual(parser.Validate, true); + Assert.AreEqual(parser.Code, 1234); + + // Falures + payload = "disarm,1a"; + parser = payload.ToCommandCode(supportValidate: true); + Assert.AreEqual(parser.Success, false); + Assert.AreEqual(parser.Command, "disarm"); + Assert.AreEqual(parser.Validate, false); + Assert.AreEqual(parser.Code, 0); + + payload = "disarm,validate,"; + parser = payload.ToCommandCode(supportValidate: true); + Assert.AreEqual(parser.Success, false); + Assert.AreEqual(parser.Command, "disarm"); + Assert.AreEqual(parser.Validate, true); + Assert.AreEqual(parser.Code, 0); + + payload = "disarm,test,1234"; + parser = payload.ToCommandCode(supportValidate: true); + Assert.AreEqual(parser.Success, false); + Assert.AreEqual(parser.Command, "disarm"); + Assert.AreEqual(parser.Validate, false); + Assert.AreEqual(parser.Code, 0); } [TestMethod] @@ -65,4 +99,4 @@ namespace OmniLinkBridgeTest CollectionAssert.AreEqual(new List(new int[] { 1, 2, 3, 5, 6 }), range); } } -} +} \ No newline at end of file diff --git a/README.md b/README.md index 754fdd3..c8fbbfa 100644 --- a/README.md +++ b/README.md @@ -166,19 +166,20 @@ string secure, trouble SUB omnilink/areaX/name string Area name -SUB omnilink/areaX/state +SUB omnilink/areaX/state string triggered, arming, armed_night, armed_night_delay, armed_home, armed_home_instant, armed_away, armed_vacation, disarmed -SUB omnilink/areaX/basic_state +SUB omnilink/areaX/basic_state string triggered, arming, armed_night, armed_home, armed_away, armed_vacation, disarmed SUB omnilink/areaX/json_state string json -PUB omnilink/areaX/command +PUB omnilink/areaX/command string arm_home, arm_away, arm_night, disarm, arm_home_instant, arm_night_delay, arm_vacation note Use area 0 for all areas note Optionally the user code number can be specified 'disarm,1' +note Optionally the security code can be be specified 'disarm,validate,1234' PUB omnilink/areaX/alarm_command string burglary, fire, auxiliary