1.1.13 - Add MQTT area security code support

This commit is contained in:
Ryan Wagoner 2022-10-20 18:10:42 -04:00
parent 7c24d9046e
commit 1ce5e3dab9
13 changed files with 180 additions and 52 deletions

View file

@ -1,6 +1,7 @@
FROM mono:latest AS build FROM mono:latest AS build
ARG TARGETPLATFORM ARG TARGETPLATFORM
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y unixodbc apt-get install -y unixodbc

View file

@ -1,4 +1,5 @@
using System; using OmniLinkBridge.MQTT;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -24,15 +25,36 @@ namespace OmniLinkBridge
return (b & (1 << pos)) != 0; 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(','); string[] payloads = payload.Split(',');
int code = 0; 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) public static string ToSpaceTitleCase(this string phrase)

View file

@ -53,6 +53,7 @@ namespace OmniLinkBridge
public static string mqtt_discovery_name_prefix; public static string mqtt_discovery_name_prefix;
public static HashSet<int> mqtt_discovery_ignore_zones; public static HashSet<int> mqtt_discovery_ignore_zones;
public static HashSet<int> mqtt_discovery_ignore_units; public static HashSet<int> mqtt_discovery_ignore_units;
public static HashSet<int> mqtt_discovery_area_code_required;
public static ConcurrentDictionary<int, MQTT.OverrideZone> mqtt_discovery_override_zone; public static ConcurrentDictionary<int, MQTT.OverrideZone> mqtt_discovery_override_zone;
// Notifications // Notifications

View file

@ -1,9 +1,16 @@
namespace OmniLinkBridge.MQTT using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace OmniLinkBridge.MQTT
{ {
public class Alarm : Device public class Alarm : Device
{ {
public string command_topic { get; set; } 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; }
} }
} }

View file

@ -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; }
}
}

View file

@ -18,8 +18,16 @@ namespace OmniLinkBridge.MQTT
unique_id = $"{Global.mqtt_prefix}area{area.Number}", unique_id = $"{Global.mqtt_prefix}area{area.Number}",
name = Global.mqtt_discovery_name_prefix + area.Name, name = Global.mqtt_discovery_name_prefix + area.Name,
state_topic = area.ToTopic(Topic.basic_state), 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; return ret;
} }
@ -177,7 +185,7 @@ namespace OmniLinkBridge.MQTT
public static string ToJsonState(this clsArea area) public static string ToJsonState(this clsArea area)
{ {
AreaState state = new AreaState() AreaState state = new AreaState
{ {
arming = area.ExitTimer > 0, arming = area.ExitTimer > 0,
burglary_alarm = area.AreaAlarms.IsBitSet(0), burglary_alarm = area.AreaAlarms.IsBitSet(0),
@ -187,10 +195,8 @@ namespace OmniLinkBridge.MQTT
freeze_alarm = area.AreaAlarms.IsBitSet(4), freeze_alarm = area.AreaAlarms.IsBitSet(4),
water_alarm = area.AreaAlarms.IsBitSet(5), water_alarm = area.AreaAlarms.IsBitSet(5),
duress_alarm = area.AreaAlarms.IsBitSet(6), duress_alarm = area.AreaAlarms.IsBitSet(6),
temperature_alarm = area.AreaAlarms.IsBitSet(7) temperature_alarm = area.AreaAlarms.IsBitSet(7),
}; mode = area.AreaMode switch
state.mode = area.AreaMode switch
{ {
enuSecurityMode.Night => "night", enuSecurityMode.Night => "night",
enuSecurityMode.NightDly => "night_delay", enuSecurityMode.NightDly => "night_delay",
@ -199,6 +205,7 @@ namespace OmniLinkBridge.MQTT
enuSecurityMode.Away => "away", enuSecurityMode.Away => "away",
enuSecurityMode.Vacation => "vacation", enuSecurityMode.Vacation => "vacation",
_ => "off", _ => "off",
}
}; };
return JsonConvert.SerializeObject(state); return JsonConvert.SerializeObject(state);
} }

View file

@ -64,20 +64,63 @@ namespace OmniLinkBridge.MQTT
private void ProcessAreaReceived(clsArea area, Topic command, string payload) private void ProcessAreaReceived(clsArea area, Topic command, string payload)
{ {
int code; AreaCommandCode parser = payload.ToCommandCode(supportValidate: true);
(payload, code) = payload.ToCommandCode();
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) if (area.Number == 0)
log.Debug("SetArea: 0 implies all areas will be changed"); log.Debug("SetArea: 0 implies all areas will be changed");
log.Debug("SetArea: {id} to {value}", area.Number, cmd.ToString().Replace("arm_", "").Replace("_", " ")); if (parser.Validate)
OmniLink.SendCommand(AreaMapping[cmd], (byte)code, (ushort)area.Number);
}
else if (command == Topic.alarm_command && area.Number > 0 && Enum.TryParse(payload, true, out AlarmCommands alarm))
{ {
log.Debug("SetAreaAlarm: {id} to {value}", area.Number, payload); 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(parser.Command, true, out AlarmCommands alarm))
{
log.Debug("SetAreaAlarm: {id} to {value}", area.Number, parser.Command);
OmniLink.Controller.Connection.Send(new clsOL2MsgActivateKeypadEmg(OmniLink.Controller.Connection) 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) private void ProcessZoneReceived(clsZone zone, Topic command, string payload)
{ {
int code; AreaCommandCode parser = payload.ToCommandCode();
(payload, code) = 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)) !(zone.Number == 0 && cmd == ZoneCommands.bypass))
{ {
if (zone.Number == 0) if (zone.Number == 0)
log.Debug("SetZone: 0 implies all zones will be restored"); log.Debug("SetZone: 0 implies all zones will be restored");
log.Debug("SetZone: {id} to {value}", zone.Number, payload); log.Debug("SetZone: {id} to {value}", zone.Number, parser.Command);
OmniLink.SendCommand(ZoneMapping[cmd], (byte)code, (ushort)zone.Number); OmniLink.SendCommand(ZoneMapping[cmd], (byte)parser.Code, (ushort)zone.Number);
} }
} }

View file

@ -84,6 +84,7 @@
<Compile Include="Modules\TimeSyncModule.cs" /> <Compile Include="Modules\TimeSyncModule.cs" />
<Compile Include="MQTT\Alarm.cs" /> <Compile Include="MQTT\Alarm.cs" />
<Compile Include="MQTT\AlarmCommands.cs" /> <Compile Include="MQTT\AlarmCommands.cs" />
<Compile Include="MQTT\AreaCommandCode.cs" />
<Compile Include="MQTT\AreaCommands.cs" /> <Compile Include="MQTT\AreaCommands.cs" />
<Compile Include="MQTT\AreaState.cs" /> <Compile Include="MQTT\AreaState.cs" />
<Compile Include="MQTT\BinarySensor.cs" /> <Compile Include="MQTT\BinarySensor.cs" />
@ -174,13 +175,13 @@
<Version>4.5.0</Version> <Version>4.5.0</Version>
</PackageReference> </PackageReference>
<PackageReference Include="MQTTnet.Extensions.ManagedClient"> <PackageReference Include="MQTTnet.Extensions.ManagedClient">
<Version>3.1.1</Version> <Version>3.1.2</Version>
</PackageReference> </PackageReference>
<PackageReference Include="Newtonsoft.Json"> <PackageReference Include="Newtonsoft.Json">
<Version>13.0.1</Version> <Version>13.0.1</Version>
</PackageReference> </PackageReference>
<PackageReference Include="Serilog"> <PackageReference Include="Serilog">
<Version>2.10.0</Version> <Version>2.12.0</Version>
</PackageReference> </PackageReference>
<PackageReference Include="Serilog.Formatting.Compact"> <PackageReference Include="Serilog.Formatting.Compact">
<Version>1.1.0</Version> <Version>1.1.0</Version>
@ -189,7 +190,7 @@
<Version>1.5.0</Version> <Version>1.5.0</Version>
</PackageReference> </PackageReference>
<PackageReference Include="Serilog.Sinks.Console"> <PackageReference Include="Serilog.Sinks.Console">
<Version>4.0.1</Version> <Version>4.1.0</Version>
</PackageReference> </PackageReference>
<PackageReference Include="Serilog.Sinks.File"> <PackageReference Include="Serilog.Sinks.File">
<Version>5.0.0</Version> <Version>5.0.0</Version>

View file

@ -52,6 +52,7 @@ mqtt_discovery_name_prefix =
# Specify a range of numbers like 1,2,3,5-10 # Specify a range of numbers like 1,2,3,5-10
mqtt_discovery_ignore_zones = mqtt_discovery_ignore_zones =
mqtt_discovery_ignore_units = mqtt_discovery_ignore_units =
mqtt_discovery_area_code_required =
# device_class must be battery, door, garage_door, gas, moisture, motion, problem, smoke, or window # 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=5;device_class=garage_door
#mqtt_discovery_override_zone = id=6;device_class=garage_door #mqtt_discovery_override_zone = id=6;device_class=garage_door

View file

@ -32,5 +32,5 @@ using System.Runtime.InteropServices;
// You can specify all the values or you can default the Build and Revision Numbers // You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below: // by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")] // [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.1.12.0")] [assembly: AssemblyVersion("1.1.13.0")]
[assembly: AssemblyFileVersion("1.1.12.0")] [assembly: AssemblyFileVersion("1.1.13.0")]

View file

@ -84,6 +84,7 @@ namespace OmniLinkBridge
Global.mqtt_discovery_ignore_zones = settings.ValidateRange("mqtt_discovery_ignore_zones"); 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_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.OverrideZone>("mqtt_discovery_override_zone"); Global.mqtt_discovery_override_zone = settings.LoadOverrideZone<MQTT.OverrideZone>("mqtt_discovery_override_zone");
} }

View file

@ -3,6 +3,7 @@ using System.Text;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using OmniLinkBridge; using OmniLinkBridge;
using OmniLinkBridge.MQTT;
namespace OmniLinkBridgeTest namespace OmniLinkBridgeTest
{ {
@ -41,18 +42,51 @@ namespace OmniLinkBridgeTest
[TestMethod] [TestMethod]
public void TestToCommandCode() public void TestToCommandCode()
{ {
string payload, command; string payload;
int code; AreaCommandCode parser;
payload = "disarm"; payload = "disarm";
(command, code) = payload.ToCommandCode(); parser = payload.ToCommandCode(supportValidate: true);
Assert.AreEqual(command, "disarm"); Assert.AreEqual(parser.Success, true);
Assert.AreEqual(code, 0); Assert.AreEqual(parser.Command, "disarm");
Assert.AreEqual(parser.Validate, false);
Assert.AreEqual(parser.Code, 0);
payload = "disarm,1"; payload = "disarm,1";
(command, code) = payload.ToCommandCode(); parser = payload.ToCommandCode(supportValidate: true);
Assert.AreEqual(command, "disarm"); Assert.AreEqual(parser.Success, true);
Assert.AreEqual(code, 1); 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] [TestMethod]

View file

@ -179,6 +179,7 @@ PUB omnilink/areaX/command
string arm_home, arm_away, arm_night, disarm, arm_home_instant, arm_night_delay, arm_vacation string arm_home, arm_away, arm_night, disarm, arm_home_instant, arm_night_delay, arm_vacation
note Use area 0 for all areas note Use area 0 for all areas
note Optionally the user code number can be specified 'disarm,1' 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 PUB omnilink/areaX/alarm_command
string burglary, fire, auxiliary string burglary, fire, auxiliary