Add MQTT lock support

This commit is contained in:
Ryan Wagoner 2024-04-10 20:53:48 -04:00
parent 495ce74149
commit 84b52c8f30
18 changed files with 219 additions and 6 deletions

View file

@ -31,6 +31,7 @@ namespace OmniLinkBridge
public static bool verbose_thermostat; public static bool verbose_thermostat;
public static bool verbose_unit; public static bool verbose_unit;
public static bool verbose_message; public static bool verbose_message;
public static bool verbose_lock;
// mySQL Logging // mySQL Logging
public static bool mysql_logging; public static bool mysql_logging;

View file

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

View file

@ -605,5 +605,35 @@ namespace OmniLinkBridge.MQTT
else else
return "off"; 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";
}
} }
} }

View file

@ -49,6 +49,8 @@ namespace OmniLinkBridge.MQTT
ProcessButtonReceived(OmniLink.Controller.Buttons[id], topic, payload); ProcessButtonReceived(OmniLink.Controller.Buttons[id], topic, payload);
else if (type == CommandTypes.message && id > 0 && id <= OmniLink.Controller.Messages.Count) else if (type == CommandTypes.message && id > 0 && id <= OmniLink.Controller.Messages.Count)
ProcessMessageReceived(OmniLink.Controller.Messages[id], topic, payload); 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<AreaCommands, enuUnitCommand> AreaMapping = new Dictionary<AreaCommands, enuUnitCommand> private static readonly IDictionary<AreaCommands, enuUnitCommand> AreaMapping = new Dictionary<AreaCommands, enuUnitCommand>
@ -294,5 +296,24 @@ namespace OmniLinkBridge.MQTT
OmniLink.SendCommand(MessageMapping[cmd], par, (ushort)message.Number); OmniLink.SendCommand(MessageMapping[cmd], par, (ushort)message.Number);
} }
} }
private static readonly IDictionary<LockCommands, enuUnitCommand> LockMapping = new Dictionary<LockCommands, enuUnitCommand>
{
{ 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);
}
}
} }
} }

View file

@ -7,6 +7,7 @@
unit, unit,
thermostat, thermostat,
button, button,
message message,
@lock
} }
} }

View file

@ -0,0 +1,8 @@
namespace OmniLinkBridge.MQTT.Parser
{
enum LockCommands
{
@lock,
unlock
}
}

View file

@ -39,6 +39,7 @@ namespace OmniLinkBridge.Modules
omnilink.OnThermostatStatus += Omnilink_OnThermostatStatus; omnilink.OnThermostatStatus += Omnilink_OnThermostatStatus;
omnilink.OnUnitStatus += Omnilink_OnUnitStatus; omnilink.OnUnitStatus += Omnilink_OnUnitStatus;
omnilink.OnMessageStatus += Omnilink_OnMessageStatus; omnilink.OnMessageStatus += Omnilink_OnMessageStatus;
omnilink.OnLockStatus += Omnilink_OnLockStatus;
omnilink.OnSystemStatus += Omnilink_OnSystemStatus; omnilink.OnSystemStatus += Omnilink_OnSystemStatus;
} }
@ -199,10 +200,24 @@ namespace OmniLinkBridge.Modules
thermostatUsage++; 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")) using (LogContext.PushProperty("Telemetry", "ControllerUsage"))
log.Debug("Controller has {AreaUsage} areas, {ZoneUsage} zones, {UnitUsage} units, " + log.Debug("Controller has {AreaUsage} areas, {ZoneUsage} zones, {UnitUsage} units, " +
"{OutputUsage} outputs, {FlagUsage} flags, {ThermostatUsage} thermostats", "{OutputUsage} outputs, {FlagUsage} flags, {ThermostatUsage} thermostats, {LockUsage} locks",
areaUsage, zoneUsage, unitUsage, outputUsage, flagUsage, thermostatUsage); areaUsage, zoneUsage, unitUsage, outputUsage, flagUsage, thermostatUsage, lockUsage);
} }
private void Omnilink_OnAreaStatus(object sender, AreaStatusEventArgs e) 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()); 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) private void Omnilink_OnSystemStatus(object sender, SystemStatusEventArgs e)
{ {
DBQueue(@" DBQueue(@"

View file

@ -50,6 +50,7 @@ namespace OmniLinkBridge.Modules
OmniLink.OnThermostatStatus += Omnilink_OnThermostatStatus; OmniLink.OnThermostatStatus += Omnilink_OnThermostatStatus;
OmniLink.OnButtonStatus += OmniLink_OnButtonStatus; OmniLink.OnButtonStatus += OmniLink_OnButtonStatus;
OmniLink.OnMessageStatus += OmniLink_OnMessageStatus; OmniLink.OnMessageStatus += OmniLink_OnMessageStatus;
OmniLink.OnLockStatus += OmniLink_OnLockStatus;
OmniLink.OnSystemStatus += OmniLink_OnSystemStatus; OmniLink.OnSystemStatus += OmniLink_OnSystemStatus;
MessageProcessor = new MessageProcessor(omni); MessageProcessor = new MessageProcessor(omni);
@ -169,6 +170,7 @@ namespace OmniLinkBridge.Modules
PublishThermostats(); PublishThermostats();
PublishButtons(); PublishButtons();
PublishMessages(); PublishMessages();
PublishLocks();
PublishControllerStatus(ONLINE); PublishControllerStatus(ONLINE);
PublishAsync($"{Global.mqtt_prefix}/model", OmniLink.Controller.GetModelText()); 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) private void Omnilink_OnAreaStatus(object sender, AreaStatusEventArgs e)
{ {
if (!MqttClient.IsConnected) if (!MqttClient.IsConnected)
@ -514,6 +539,14 @@ namespace OmniLinkBridge.Modules
PublishMessageStateAsync(e.Message); 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) private void OmniLink_OnSystemStatus(object sender, SystemStatusEventArgs e)
{ {
if (!MqttClient.IsConnected) if (!MqttClient.IsConnected)
@ -590,6 +623,11 @@ namespace OmniLinkBridge.Modules
return PublishAsync(message.ToTopic(Topic.state), message.ToState()); 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) private Task PublishAsync(string topic, string payload)
{ {
return MqttClient.PublishAsync(topic, payload, MqttQualityOfServiceLevel.AtMostOnce, true); return MqttClient.PublishAsync(topic, payload, MqttQualityOfServiceLevel.AtMostOnce, true);

View file

@ -40,6 +40,7 @@ namespace OmniLinkBridge.Modules
public event EventHandler<UnitStatusEventArgs> OnUnitStatus; public event EventHandler<UnitStatusEventArgs> OnUnitStatus;
public event EventHandler<ButtonStatusEventArgs> OnButtonStatus; public event EventHandler<ButtonStatusEventArgs> OnButtonStatus;
public event EventHandler<MessageStatusEventArgs> OnMessageStatus; public event EventHandler<MessageStatusEventArgs> OnMessageStatus;
public event EventHandler<LockStatusEventArgs> OnLockStatus;
public event EventHandler<SystemStatusEventArgs> OnSystemStatus; public event EventHandler<SystemStatusEventArgs> OnSystemStatus;
private readonly AutoResetEvent trigger = new AutoResetEvent(false); private readonly AutoResetEvent trigger = new AutoResetEvent(false);
@ -214,6 +215,7 @@ namespace OmniLinkBridge.Modules
await GetNamed(enuObjectType.Unit); await GetNamed(enuObjectType.Unit);
await GetNamed(enuObjectType.Message); await GetNamed(enuObjectType.Message);
await GetNamed(enuObjectType.Button); await GetNamed(enuObjectType.Button);
await GetNamed(enuObjectType.AccessControlReader);
} }
private async Task GetSystemFormats() private async Task GetSystemFormats()
@ -342,6 +344,9 @@ namespace OmniLinkBridge.Modules
case enuObjectType.Button: case enuObjectType.Button:
Controller.Buttons.CopyProperties(MSG); Controller.Buttons.CopyProperties(MSG);
break; break;
case enuObjectType.AccessControlReader:
Controller.AccessControlReaders.CopyProperties(MSG);
break;
default: default:
break; break;
} }
@ -679,6 +684,17 @@ namespace OmniLinkBridge.Modules
}); });
} }
break; 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: default:
if (Global.verbose_unhandled) if (Global.verbose_unhandled)
{ {

View file

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

View file

@ -83,6 +83,7 @@
<Compile Include="CoreServer.cs" /> <Compile Include="CoreServer.cs" />
<Compile Include="Modules\TimeSyncModule.cs" /> <Compile Include="Modules\TimeSyncModule.cs" />
<Compile Include="MQTT\HomeAssistant\Alarm.cs" /> <Compile Include="MQTT\HomeAssistant\Alarm.cs" />
<Compile Include="MQTT\HomeAssistant\Lock.cs" />
<Compile Include="MQTT\OverrideUnit.cs" /> <Compile Include="MQTT\OverrideUnit.cs" />
<Compile Include="MQTT\Parser\AlarmCommands.cs" /> <Compile Include="MQTT\Parser\AlarmCommands.cs" />
<Compile Include="MQTT\AreaCommandCode.cs" /> <Compile Include="MQTT\AreaCommandCode.cs" />
@ -95,6 +96,7 @@
<Compile Include="MQTT\HomeAssistant\Climate.cs" /> <Compile Include="MQTT\HomeAssistant\Climate.cs" />
<Compile Include="MQTT\HomeAssistant\DeviceRegistry.cs" /> <Compile Include="MQTT\HomeAssistant\DeviceRegistry.cs" />
<Compile Include="MQTT\Extensions.cs" /> <Compile Include="MQTT\Extensions.cs" />
<Compile Include="MQTT\Parser\LockCommands.cs" />
<Compile Include="MQTT\Parser\MessageCommands.cs" /> <Compile Include="MQTT\Parser\MessageCommands.cs" />
<Compile Include="MQTT\MessageProcessor.cs" /> <Compile Include="MQTT\MessageProcessor.cs" />
<Compile Include="MQTT\HomeAssistant\Number.cs" /> <Compile Include="MQTT\HomeAssistant\Number.cs" />
@ -115,6 +117,7 @@
<Compile Include="OmniLink\ButtonStatusEventArgs.cs" /> <Compile Include="OmniLink\ButtonStatusEventArgs.cs" />
<Compile Include="OmniLink\IOmniLinkII.cs" /> <Compile Include="OmniLink\IOmniLinkII.cs" />
<Compile Include="OmniLink\SystemEventType.cs" /> <Compile Include="OmniLink\SystemEventType.cs" />
<Compile Include="OmniLink\LockStatusEventArgs.cs" />
<Compile Include="OmniLink\UnitStatusEventArgs.cs" /> <Compile Include="OmniLink\UnitStatusEventArgs.cs" />
<Compile Include="OmniLink\ThermostatStatusEventArgs.cs" /> <Compile Include="OmniLink\ThermostatStatusEventArgs.cs" />
<Compile Include="OmniLink\MessageStatusEventArgs.cs" /> <Compile Include="OmniLink\MessageStatusEventArgs.cs" />

View file

@ -22,6 +22,7 @@ verbose_thermostat_timer = yes
verbose_thermostat = yes verbose_thermostat = yes
verbose_unit = yes verbose_unit = yes
verbose_message = yes verbose_message = yes
verbose_lock = yes
# mySQL Logging (yes/no) # mySQL Logging (yes/no)
mysql_logging = no mysql_logging = no

View file

@ -10,7 +10,7 @@ using System.Runtime.InteropServices;
[assembly: AssemblyConfiguration("")] [assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Excalibur Partners, LLC")] [assembly: AssemblyCompany("Excalibur Partners, LLC")]
[assembly: AssemblyProduct("OmniLinkBridge")] [assembly: AssemblyProduct("OmniLinkBridge")]
[assembly: AssemblyCopyright("Copyright © Excalibur Partners, LLC 2022")] [assembly: AssemblyCopyright("Copyright © Excalibur Partners, LLC 2024")]
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]

View file

@ -52,6 +52,7 @@ namespace OmniLinkBridge
Global.verbose_thermostat = settings.ValidateBool("verbose_thermostat"); Global.verbose_thermostat = settings.ValidateBool("verbose_thermostat");
Global.verbose_unit = settings.ValidateBool("verbose_unit"); Global.verbose_unit = settings.ValidateBool("verbose_unit");
Global.verbose_message = settings.ValidateBool("verbose_message"); Global.verbose_message = settings.ValidateBool("verbose_message");
Global.verbose_lock = settings.ValidateBool("verbose_lock");
// mySQL Logging // mySQL Logging
Global.mysql_logging = settings.ValidateBool("mysql_logging"); Global.mysql_logging = settings.ValidateBool("mysql_logging");

View file

@ -286,6 +286,33 @@ namespace OmniLinkBridgeTest
check(2, "SHOW", enuUnitCommand.ShowMsgWBeep, 0); 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);
}
} }
} }

View file

@ -7,7 +7,7 @@ using System.Runtime.InteropServices;
[assembly: AssemblyConfiguration("")] [assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Excalibur Partners, LLC")] [assembly: AssemblyCompany("Excalibur Partners, LLC")]
[assembly: AssemblyProduct("OmniLinkBridgeTest")] [assembly: AssemblyProduct("OmniLinkBridgeTest")]
[assembly: AssemblyCopyright("Copyright © Excalibur Partners, LLC 2022")] [assembly: AssemblyCopyright("Copyright © Excalibur Partners, LLC 2024")]
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]

View file

@ -78,7 +78,8 @@ namespace OmniLinkBridgeTest
"verbose_thermostat_timer", "verbose_thermostat_timer",
"verbose_thermostat", "verbose_thermostat",
"verbose_unit", "verbose_unit",
"verbose_message" "verbose_message",
"verbose_lock"
}) })
{ {
List<string> lines = new List<string>(RequiredSettings()) List<string> lines = new List<string>(RequiredSettings())

View file

@ -312,6 +312,18 @@ PUB omnilink/messageX/command
string show, show_no_beep, show_no_beep_or_led, clear 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 ## 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. To test the web service API you can use your browser to view a page or PowerShell (see below) to change a value.