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
ARG TARGETPLATFORM
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
RUN apt-get update && \
apt-get install -y unixodbc

View file

@ -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)

View file

@ -53,6 +53,7 @@ namespace OmniLinkBridge
public static string mqtt_discovery_name_prefix;
public static HashSet<int> mqtt_discovery_ignore_zones;
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;
// 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 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}",
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;
}
@ -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,10 +195,8 @@ 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
temperature_alarm = area.AreaAlarms.IsBitSet(7),
mode = area.AreaMode switch
{
enuSecurityMode.Night => "night",
enuSecurityMode.NightDly => "night_delay",
@ -199,6 +205,7 @@ namespace OmniLinkBridge.MQTT
enuSecurityMode.Away => "away",
enuSecurityMode.Vacation => "vacation",
_ => "off",
}
};
return JsonConvert.SerializeObject(state);
}

View file

@ -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);
}
else if (command == Topic.alarm_command && area.Number > 0 && Enum.TryParse(payload, true, out AlarmCommands alarm))
if (parser.Validate)
{
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)
{
@ -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);
}
}

View file

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

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
// 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")]

View file

@ -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.OverrideZone>("mqtt_discovery_override_zone");
}

View file

@ -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]

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
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