1.1.14 - Improve MQTT unit, output and flag capabilities

This commit is contained in:
Ryan Wagoner 2022-11-02 18:09:33 -04:00
parent a016e1cd64
commit 495ce74149
41 changed files with 388 additions and 150 deletions

View file

@ -1,5 +1,4 @@
using OmniLinkBridge.MQTT; using System;
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,39 +23,6 @@ namespace OmniLinkBridge
{ {
return (b & (1 << pos)) != 0; return (b & (1 << pos)) != 0;
} }
public static AreaCommandCode ToCommandCode(this string payload, bool supportValidate = false)
{
string[] payloads = payload.Split(',');
int code = 0;
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)
{ {
return Regex.Replace(phrase, "(\\B[A-Z])", " $1"); return Regex.Replace(phrase, "(\\B[A-Z])", " $1");

View file

@ -55,6 +55,7 @@ namespace OmniLinkBridge
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 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;
public static ConcurrentDictionary<int, MQTT.OverrideUnit> mqtt_discovery_override_unit;
// Notifications // Notifications
public static bool notify_area; public static bool notify_area;

View file

@ -0,0 +1,65 @@
using HAI_Shared;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OmniLinkBridge.MQTT
{
public static class Extensions
{
public static AreaCommandCode ToCommandCode(this string payload, bool supportValidate = false)
{
string[] payloads = payload.Split(',');
int code = 0;
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 UnitType ToUnitType(this clsUnit unit)
{
Global.mqtt_discovery_override_unit.TryGetValue(unit.Number, out OverrideUnit override_unit);
if (unit.Type == enuOL2UnitType.Output)
return UnitType.@switch;
if (unit.Type == enuOL2UnitType.Flag)
{
if (override_unit != null && override_unit.type == UnitType.number)
return UnitType.number;
return UnitType.@switch;
}
if (override_unit != null && override_unit.type == UnitType.@switch)
return UnitType.@switch;
return UnitType.light;
}
}
}

View file

@ -1,7 +1,7 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
namespace OmniLinkBridge.MQTT namespace OmniLinkBridge.MQTT.HomeAssistant
{ {
public class Alarm : Device public class Alarm : Device
{ {

View file

@ -1,7 +1,7 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
namespace OmniLinkBridge.MQTT namespace OmniLinkBridge.MQTT.HomeAssistant
{ {
public class BinarySensor : Device public class BinarySensor : Device
{ {

View file

@ -1,6 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace OmniLinkBridge.MQTT namespace OmniLinkBridge.MQTT.HomeAssistant
{ {
public class Climate : Device public class Climate : Device
{ {

View file

@ -3,7 +3,7 @@ using Newtonsoft.Json.Converters;
using OmniLinkBridge.Modules; using OmniLinkBridge.Modules;
using System.Collections.Generic; using System.Collections.Generic;
namespace OmniLinkBridge.MQTT namespace OmniLinkBridge.MQTT.HomeAssistant
{ {
public class Device public class Device
{ {

View file

@ -1,4 +1,4 @@
namespace OmniLinkBridge.MQTT namespace OmniLinkBridge.MQTT.HomeAssistant
{ {
public class DeviceRegistry public class DeviceRegistry
{ {

View file

@ -1,4 +1,4 @@
namespace OmniLinkBridge.MQTT namespace OmniLinkBridge.MQTT.HomeAssistant
{ {
public class Light : Device public class Light : Device
{ {

View file

@ -0,0 +1,18 @@
using Newtonsoft.Json;
namespace OmniLinkBridge.MQTT.HomeAssistant
{
public class Number : Device
{
public string command_topic { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string icon { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public int? min { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public int? max { get; set; }
}
}

View file

@ -1,7 +1,7 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
namespace OmniLinkBridge.MQTT namespace OmniLinkBridge.MQTT.HomeAssistant
{ {
public class Sensor : Device public class Sensor : Device
{ {

View file

@ -1,6 +1,6 @@
using Newtonsoft.Json; using Newtonsoft.Json;
namespace OmniLinkBridge.MQTT namespace OmniLinkBridge.MQTT.HomeAssistant
{ {
public class Switch : Device public class Switch : Device
{ {

View file

@ -1,6 +1,8 @@
using HAI_Shared; using HAI_Shared;
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Collections.Generic; using System.Collections.Generic;
using OmniLinkBridge.MQTT.HomeAssistant;
using OmniLinkBridge.MQTT.Parser;
namespace OmniLinkBridge.MQTT namespace OmniLinkBridge.MQTT
{ {
@ -399,6 +401,20 @@ namespace OmniLinkBridge.MQTT
return ret; return ret;
} }
public static Number ToConfigNumber(this clsUnit unit)
{
Number ret = new Number
{
unique_id = $"{Global.mqtt_prefix}unit{unit.Number}number",
name = Global.mqtt_discovery_name_prefix + unit.Name,
state_topic = unit.ToTopic(Topic.flag_state),
command_topic = unit.ToTopic(Topic.flag_command),
min = 0,
max = 255
};
return ret;
}
public static string ToState(this clsUnit unit) public static string ToState(this clsUnit unit)
{ {
return unit.Status == 0 || unit.Status == 100 ? UnitCommands.OFF.ToString() : UnitCommands.ON.ToString(); return unit.Status == 0 || unit.Status == 100 ? UnitCommands.OFF.ToString() : UnitCommands.ON.ToString();

View file

@ -1,4 +1,5 @@
using HAI_Shared; using HAI_Shared;
using OmniLinkBridge.MQTT.Parser;
using OmniLinkBridge.OmniLink; using OmniLinkBridge.OmniLink;
using Serilog; using Serilog;
using System; using System;
@ -167,10 +168,16 @@ namespace OmniLinkBridge.MQTT
OmniLink.SendCommand(UnitMapping[cmd], 0, (ushort)unit.Number); OmniLink.SendCommand(UnitMapping[cmd], 0, (ushort)unit.Number);
} }
} }
else if (command == Topic.brightness_command && int.TryParse(payload, out int unitValue)) else if (unit.Type == enuOL2UnitType.Flag &&
command == Topic.flag_command && int.TryParse(payload, out int flagValue))
{
log.Debug("SetUnit: {id} to {value}", unit.Number, payload);
OmniLink.SendCommand(enuUnitCommand.Set, BitConverter.GetBytes(flagValue)[0], (ushort)unit.Number);
}
else if (unit.Type != enuOL2UnitType.Output &&
command == Topic.brightness_command && int.TryParse(payload, out int unitValue))
{ {
log.Debug("SetUnit: {id} to {value}%", unit.Number, payload); log.Debug("SetUnit: {id} to {value}%", unit.Number, payload);
OmniLink.SendCommand(enuUnitCommand.Level, BitConverter.GetBytes(unitValue)[0], (ushort)unit.Number); OmniLink.SendCommand(enuUnitCommand.Level, BitConverter.GetBytes(unitValue)[0], (ushort)unit.Number);
// Force status change instead of waiting on controller to update // Force status change instead of waiting on controller to update
@ -178,10 +185,10 @@ namespace OmniLinkBridge.MQTT
// which will cause light to go to 100% brightness // which will cause light to go to 100% brightness
unit.Status = (byte)(100 + unitValue); unit.Status = (byte)(100 + unitValue);
} }
else if (command == Topic.scene_command && char.TryParse(payload, out char scene)) else if (unit.Type != enuOL2UnitType.Output &&
command == Topic.scene_command && char.TryParse(payload, out char scene))
{ {
log.Debug("SetUnit: {id} to {value}", unit.Number, payload); log.Debug("SetUnit: {id} to {value}", unit.Number, payload);
OmniLink.SendCommand(enuUnitCommand.Compose, (byte)(scene - 63), (ushort)unit.Number); OmniLink.SendCommand(enuUnitCommand.Compose, (byte)(scene - 63), (ushort)unit.Number);
} }
} }

View file

@ -1,12 +0,0 @@
using Newtonsoft.Json;
namespace OmniLinkBridge.MQTT
{
public class Number : Device
{
public string command_topic { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string icon { get; set; }
}
}

View file

@ -0,0 +1,7 @@
namespace OmniLinkBridge.MQTT
{
public class OverrideUnit
{
public UnitType type { get; set; }
}
}

View file

@ -1,4 +1,6 @@
namespace OmniLinkBridge.MQTT using OmniLinkBridge.MQTT.HomeAssistant;
namespace OmniLinkBridge.MQTT
{ {
public class OverrideZone public class OverrideZone
{ {

View file

@ -1,4 +1,4 @@
namespace OmniLinkBridge.MQTT namespace OmniLinkBridge.MQTT.Parser
{ {
enum AlarmCommands enum AlarmCommands
{ {

View file

@ -1,4 +1,4 @@
namespace OmniLinkBridge.MQTT namespace OmniLinkBridge.MQTT.Parser
{ {
enum AreaCommands enum AreaCommands
{ {

View file

@ -1,4 +1,4 @@
namespace OmniLinkBridge.MQTT namespace OmniLinkBridge.MQTT.Parser
{ {
enum CommandTypes enum CommandTypes
{ {

View file

@ -1,4 +1,4 @@
namespace OmniLinkBridge.MQTT namespace OmniLinkBridge.MQTT.Parser
{ {
enum MessageCommands enum MessageCommands
{ {

View file

@ -1,4 +1,4 @@
namespace OmniLinkBridge.MQTT namespace OmniLinkBridge.MQTT.Parser
{ {
public enum Topic public enum Topic
{ {
@ -11,6 +11,8 @@
json_state, json_state,
brightness_state, brightness_state,
brightness_command, brightness_command,
flag_state,
flag_command,
scene_state, scene_state,
scene_command, scene_command,
current_operation, current_operation,

View file

@ -1,4 +1,4 @@
namespace OmniLinkBridge.MQTT namespace OmniLinkBridge.MQTT.Parser
{ {
enum UnitCommands enum UnitCommands
{ {

View file

@ -1,4 +1,4 @@
namespace OmniLinkBridge.MQTT namespace OmniLinkBridge.MQTT.Parser
{ {
enum ZoneCommands enum ZoneCommands
{ {

View file

@ -0,0 +1,9 @@
namespace OmniLinkBridge.MQTT
{
public enum UnitType
{
@switch,
light,
number
}
}

View file

@ -2,6 +2,7 @@
using OmniLinkBridge.Notifications; using OmniLinkBridge.Notifications;
using OmniLinkBridge.OmniLink; using OmniLinkBridge.OmniLink;
using Serilog; using Serilog;
using Serilog.Context;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data; using System.Data;
@ -125,15 +126,18 @@ namespace OmniLinkBridge.Modules
private void Omnilink_OnConnect(object sender, EventArgs e) private void Omnilink_OnConnect(object sender, EventArgs e)
{ {
if (Global.verbose_area) ushort areaUsage = 0;
for (ushort i = 1; i <= omnilink.Controller.Areas.Count; i++)
{ {
for (ushort i = 1; i <= omnilink.Controller.Areas.Count; i++) clsArea area = omnilink.Controller.Areas[i];
if (i > 1 && area.DefaultProperties == true)
continue;
areaUsage++;
if (Global.verbose_area)
{ {
clsArea area = omnilink.Controller.Areas[i];
if (i > 1 && area.DefaultProperties == true)
continue;
string status = area.ModeText(); string status = area.ModeText();
if (area.ExitTimer > 0) if (area.ExitTimer > 0)
@ -146,21 +150,59 @@ namespace OmniLinkBridge.Modules
} }
} }
if (Global.verbose_zone) ushort zoneUsage = 0;
for (ushort i = 1; i <= omnilink.Controller.Zones.Count; i++)
{ {
for (ushort i = 1; i <= omnilink.Controller.Zones.Count; i++) clsZone zone = omnilink.Controller.Zones[i];
if (zone.DefaultProperties == true)
continue;
zoneUsage++;
if (Global.verbose_zone)
{ {
clsZone zone = omnilink.Controller.Zones[i];
if (zone.DefaultProperties == true)
continue;
if (zone.IsTemperatureZone()) if (zone.IsTemperatureZone())
log.Verbose("Initial ZoneStatus {id} {name}, Temp: {temp}", i, zone.Name, zone.TempText()); log.Verbose("Initial ZoneStatus {id} {name}, Temp: {temp}", i, zone.Name, zone.TempText());
else else
log.Verbose("Initial ZoneStatus {id} {name}, Status: {status}", i, zone.Name, zone.StatusText()); log.Verbose("Initial ZoneStatus {id} {name}, Status: {status}", i, zone.Name, zone.StatusText());
} }
} }
ushort unitUsage = 0, outputUsage = 0, flagUsage = 0;
for (ushort i = 1; i <= omnilink.Controller.Units.Count; i++)
{
clsUnit unit = omnilink.Controller.Units[i];
if (unit.DefaultProperties == true)
continue;
if (unit.Type == enuOL2UnitType.Output)
outputUsage++;
else if (unit.Type == enuOL2UnitType.Flag)
flagUsage++;
else
unitUsage++;
if (Global.verbose_unit)
log.Verbose("Initial UnitStatus {id} {name}, Status: {status}", i, unit.Name, unit.StatusText);
}
ushort thermostatUsage = 0;
for (ushort i = 1; i <= omnilink.Controller.Thermostats.Count; i++)
{
clsThermostat thermostat = omnilink.Controller.Thermostats[i];
if (thermostat.DefaultProperties == true)
continue;
thermostatUsage++;
}
using (LogContext.PushProperty("Telemetry", "ControllerUsage"))
log.Debug("Controller has {AreaUsage} areas, {ZoneUsage} zones, {UnitUsage} units, " +
"{OutputUsage} outputs, {FlagUsage} flags, {ThermostatUsage} thermostats",
areaUsage, zoneUsage, unitUsage, outputUsage, flagUsage, thermostatUsage);
} }
private void Omnilink_OnAreaStatus(object sender, AreaStatusEventArgs e) private void Omnilink_OnAreaStatus(object sender, AreaStatusEventArgs e)

View file

@ -9,10 +9,13 @@ using MQTTnet.Extensions.ManagedClient;
using MQTTnet.Protocol; using MQTTnet.Protocol;
using Newtonsoft.Json; using Newtonsoft.Json;
using OmniLinkBridge.MQTT; using OmniLinkBridge.MQTT;
using OmniLinkBridge.MQTT.HomeAssistant;
using OmniLinkBridge.MQTT.Parser;
using OmniLinkBridge.OmniLink; using OmniLinkBridge.OmniLink;
using Serilog; using Serilog;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
@ -108,6 +111,7 @@ namespace OmniLinkBridge.Modules
Topic.command, Topic.command,
Topic.alarm_command, Topic.alarm_command,
Topic.brightness_command, Topic.brightness_command,
Topic.flag_command,
Topic.scene_command, Topic.scene_command,
Topic.temperature_heat_command, Topic.temperature_heat_command,
Topic.temperature_cool_command, Topic.temperature_cool_command,
@ -216,8 +220,8 @@ namespace OmniLinkBridge.Modules
{ {
clsArea area = OmniLink.Controller.Areas[i]; clsArea area = OmniLink.Controller.Areas[i];
// PC Access doesn't let you customize the area name for the Omni LTe or Omni IIe // PC Access doesn't let you customize the area name when configured for one area.
// (configured for 1 area). To workaround ignore default properties for the first area. // Ignore default properties for the first area.
if (i > 1 && area.DefaultProperties == true) if (i > 1 && area.DefaultProperties == true)
{ {
PublishAsync(area.ToTopic(Topic.name), null); PublishAsync(area.ToTopic(Topic.name), null);
@ -308,6 +312,7 @@ namespace OmniLinkBridge.Modules
for (ushort i = 1; i <= OmniLink.Controller.Units.Count; i++) for (ushort i = 1; i <= OmniLink.Controller.Units.Count; i++)
{ {
clsUnit unit = OmniLink.Controller.Units[i]; clsUnit unit = OmniLink.Controller.Units[i];
UnitType unitType = unit.ToUnitType();
if (unit.DefaultProperties == true) if (unit.DefaultProperties == true)
{ {
@ -321,17 +326,26 @@ namespace OmniLinkBridge.Modules
if (unit.DefaultProperties == true || Global.mqtt_discovery_ignore_units.Contains(unit.Number)) if (unit.DefaultProperties == true || Global.mqtt_discovery_ignore_units.Contains(unit.Number))
{ {
string type = i < 385 ? "light" : "switch"; foreach(UnitType entry in Enum.GetValues(typeof(UnitType)))
PublishAsync($"{Global.mqtt_discovery_prefix}/{type}/{Global.mqtt_prefix}/unit{i}/config", null); PublishAsync($"{Global.mqtt_discovery_prefix}/{entry}/{Global.mqtt_prefix}/unit{i}/config", null);
continue; continue;
} }
if (i < 385) foreach (UnitType entry in Enum.GetValues(typeof(UnitType)).Cast<UnitType>().Where(x => x != unitType))
PublishAsync($"{Global.mqtt_discovery_prefix}/light/{Global.mqtt_prefix}/unit{i}/config", PublishAsync($"{Global.mqtt_discovery_prefix}/{entry}/{Global.mqtt_prefix}/unit{i}/config", null);
JsonConvert.SerializeObject(unit.ToConfig()));
else log.Verbose("Publishing {type} {id} {name} as {unitType}", "units", i, unit.Name, unitType);
PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/unit{i}/config",
if (unitType == UnitType.@switch)
PublishAsync($"{Global.mqtt_discovery_prefix}/{unitType}/{Global.mqtt_prefix}/unit{i}/config",
JsonConvert.SerializeObject(unit.ToConfigSwitch())); JsonConvert.SerializeObject(unit.ToConfigSwitch()));
else if (unitType == UnitType.light)
PublishAsync($"{Global.mqtt_discovery_prefix}/{unitType}/{Global.mqtt_prefix}/unit{i}/config",
JsonConvert.SerializeObject(unit.ToConfig()));
else if (unitType == UnitType.number)
PublishAsync($"{Global.mqtt_discovery_prefix}/{unitType}/{Global.mqtt_prefix}/unit{i}/config",
JsonConvert.SerializeObject(unit.ToConfigNumber()));
} }
} }
@ -537,7 +551,11 @@ namespace OmniLinkBridge.Modules
{ {
PublishAsync(unit.ToTopic(Topic.state), unit.ToState()); PublishAsync(unit.ToTopic(Topic.state), unit.ToState());
if (unit.Number < 385) if (unit.Type == enuOL2UnitType.Flag)
{
PublishAsync(unit.ToTopic(Topic.flag_state), ((ushort)unit.Status).ToString());
}
else if(unit.Type != enuOL2UnitType.Output)
{ {
PublishAsync(unit.ToTopic(Topic.brightness_state), unit.ToBrightnessState().ToString()); PublishAsync(unit.ToTopic(Topic.brightness_state), unit.ToBrightnessState().ToString());
PublishAsync(unit.ToTopic(Topic.scene_state), unit.ToSceneState()); PublishAsync(unit.ToTopic(Topic.scene_state), unit.ToSceneState());

View file

@ -51,7 +51,7 @@ namespace OmniLinkBridge.Modules
Controller.Connection.NetworkAddress = address; Controller.Connection.NetworkAddress = address;
Controller.Connection.NetworkPort = (ushort)port; Controller.Connection.NetworkPort = (ushort)port;
Controller.Connection.ControllerKey = clsUtil.HexString2ByteArray(String.Concat(key1, key2)); Controller.Connection.ControllerKey = clsUtil.HexString2ByteArray(string.Concat(key1, key2));
Controller.PreferredNetworkProtocol = clsHAC.enuPreferredNetworkProtocol.TCP; Controller.PreferredNetworkProtocol = clsHAC.enuPreferredNetworkProtocol.TCP;
Controller.Connection.ConnectionType = enuOmniLinkConnectionType.Network_TCP; Controller.Connection.ConnectionType = enuOmniLinkConnectionType.Network_TCP;
@ -316,7 +316,8 @@ namespace OmniLinkBridge.Modules
Controller.Zones.CopyProperties(MSG); Controller.Zones.CopyProperties(MSG);
if (Controller.Zones[MSG.ObjectNumber].IsTemperatureZone() || Controller.Zones[MSG.ObjectNumber].IsHumidityZone()) if (Controller.Zones[MSG.ObjectNumber].IsTemperatureZone() || Controller.Zones[MSG.ObjectNumber].IsHumidityZone())
Controller.Connection.Send(new clsOL2MsgRequestExtendedStatus(Controller.Connection, enuObjectType.Auxillary, MSG.ObjectNumber, MSG.ObjectNumber), HandleRequestAuxillaryStatus); Controller.Connection.Send(new clsOL2MsgRequestExtendedStatus(Controller.Connection,
enuObjectType.Auxillary, MSG.ObjectNumber, MSG.ObjectNumber), HandleRequestAuxillaryStatus);
break; break;
case enuObjectType.Thermostat: case enuObjectType.Thermostat:
@ -327,7 +328,8 @@ namespace OmniLinkBridge.Modules
else else
tstats[MSG.ObjectNumber] = DateTime.MinValue; tstats[MSG.ObjectNumber] = DateTime.MinValue;
Controller.Connection.Send(new clsOL2MsgRequestExtendedStatus(Controller.Connection, enuObjectType.Thermostat, MSG.ObjectNumber, MSG.ObjectNumber), HandleRequestThermostatStatus); Controller.Connection.Send(new clsOL2MsgRequestExtendedStatus(Controller.Connection,
enuObjectType.Thermostat, MSG.ObjectNumber, MSG.ObjectNumber), HandleRequestThermostatStatus);
log.Debug("Added thermostat to watch list {thermostatName}", log.Debug("Added thermostat to watch list {thermostatName}",
Controller.Thermostats[MSG.ObjectNumber].Name); Controller.Thermostats[MSG.ObjectNumber].Name);
break; break;
@ -714,7 +716,8 @@ namespace OmniLinkBridge.Modules
(Controller.Connection.ConnectionState == enuOmniLinkConnectionState.Online || (Controller.Connection.ConnectionState == enuOmniLinkConnectionState.Online ||
Controller.Connection.ConnectionState == enuOmniLinkConnectionState.OnlineSecure)) Controller.Connection.ConnectionState == enuOmniLinkConnectionState.OnlineSecure))
{ {
Controller.Connection.Send(new clsOL2MsgRequestExtendedStatus(Controller.Connection, enuObjectType.Thermostat, tstat.Key, tstat.Key), HandleRequestThermostatStatus); Controller.Connection.Send(new clsOL2MsgRequestExtendedStatus(Controller.Connection,
enuObjectType.Thermostat, tstat.Key, tstat.Key), HandleRequestThermostatStatus);
if (Global.verbose_thermostat_timer) if (Global.verbose_thermostat_timer)
log.Debug("Polling status for Thermostat {thermostatName}", log.Debug("Polling status for Thermostat {thermostatName}",

View file

@ -74,14 +74,15 @@ namespace OmniLinkBridge.Modules
// Extract the 2 digit prefix to use when parsing the time // Extract the 2 digit prefix to use when parsing the time
int year = DateTime.Now.Year / 100; int year = DateTime.Now.Year / 100;
time = new DateTime((int)MSG.Year + (year * 100), (int)MSG.Month, (int)MSG.Day, (int)MSG.Hour, (int)MSG.Minute, (int)MSG.Second); time = new DateTime(MSG.Year + (year * 100), MSG.Month, MSG.Day, MSG.Hour, MSG.Minute, MSG.Second);
} }
catch catch
{ {
log.Warning("Controller time could not be parsed"); log.Warning("Controller time could not be parsed");
DateTime now = DateTime.Now; DateTime now = DateTime.Now;
OmniLink.Controller.Connection.Send(new clsOL2MsgSetTime(OmniLink.Controller.Connection, (byte)(now.Year % 100), (byte)now.Month, (byte)now.Day, (byte)now.DayOfWeek, OmniLink.Controller.Connection.Send(new clsOL2MsgSetTime(OmniLink.Controller.Connection,
(byte)(now.Year % 100), (byte)now.Month, (byte)now.Day, (byte)now.DayOfWeek,
(byte)now.Hour, (byte)now.Minute, (byte)(now.IsDaylightSavingTime() ? 1 : 0)), HandleSetTime); (byte)now.Hour, (byte)now.Minute, (byte)(now.IsDaylightSavingTime() ? 1 : 0)), HandleSetTime);
return; return;
@ -92,10 +93,11 @@ namespace OmniLinkBridge.Modules
if (adj > Global.time_drift) if (adj > Global.time_drift)
{ {
log.Warning("Controller time {controllerTime} out of sync by {driftSeconds} seconds", log.Warning("Controller time {controllerTime} out of sync by {driftSeconds} seconds",
time.ToString("MM/dd/yyyy HH:mm:ss"), adj); time.ToString("MM/dd/yyyy HH:mm:ss"), adj);
DateTime now = DateTime.Now; DateTime now = DateTime.Now;
OmniLink.Controller.Connection.Send(new clsOL2MsgSetTime(OmniLink.Controller.Connection, (byte)(now.Year % 100), (byte)now.Month, (byte)now.Day, (byte)now.DayOfWeek, OmniLink.Controller.Connection.Send(new clsOL2MsgSetTime(OmniLink.Controller.Connection,
(byte)(now.Year % 100), (byte)now.Month, (byte)now.Day, (byte)now.DayOfWeek,
(byte)now.Hour, (byte)now.Minute, (byte)(now.IsDaylightSavingTime() ? 1 : 0)), HandleSetTime); (byte)now.Hour, (byte)now.Minute, (byte)(now.IsDaylightSavingTime() ? 1 : 0)), HandleSetTime);
} }
} }

View file

@ -82,28 +82,31 @@
<ItemGroup> <ItemGroup>
<Compile Include="CoreServer.cs" /> <Compile Include="CoreServer.cs" />
<Compile Include="Modules\TimeSyncModule.cs" /> <Compile Include="Modules\TimeSyncModule.cs" />
<Compile Include="MQTT\Alarm.cs" /> <Compile Include="MQTT\HomeAssistant\Alarm.cs" />
<Compile Include="MQTT\AlarmCommands.cs" /> <Compile Include="MQTT\OverrideUnit.cs" />
<Compile Include="MQTT\Parser\AlarmCommands.cs" />
<Compile Include="MQTT\AreaCommandCode.cs" /> <Compile Include="MQTT\AreaCommandCode.cs" />
<Compile Include="MQTT\AreaCommands.cs" /> <Compile Include="MQTT\Parser\AreaCommands.cs" />
<Compile Include="MQTT\AreaState.cs" /> <Compile Include="MQTT\AreaState.cs" />
<Compile Include="MQTT\Availability.cs" /> <Compile Include="MQTT\Availability.cs" />
<Compile Include="MQTT\BinarySensor.cs" /> <Compile Include="MQTT\HomeAssistant\BinarySensor.cs" />
<Compile Include="MQTT\CommandTypes.cs" /> <Compile Include="MQTT\Parser\CommandTypes.cs" />
<Compile Include="MQTT\Device.cs" /> <Compile Include="MQTT\HomeAssistant\Device.cs" />
<Compile Include="MQTT\Climate.cs" /> <Compile Include="MQTT\HomeAssistant\Climate.cs" />
<Compile Include="MQTT\DeviceRegistry.cs" /> <Compile Include="MQTT\HomeAssistant\DeviceRegistry.cs" />
<Compile Include="MQTT\MessageCommands.cs" /> <Compile Include="MQTT\Extensions.cs" />
<Compile Include="MQTT\Parser\MessageCommands.cs" />
<Compile Include="MQTT\MessageProcessor.cs" /> <Compile Include="MQTT\MessageProcessor.cs" />
<Compile Include="MQTT\Number.cs" /> <Compile Include="MQTT\HomeAssistant\Number.cs" />
<Compile Include="MQTT\OverrideZone.cs" /> <Compile Include="MQTT\OverrideZone.cs" />
<Compile Include="MQTT\Switch.cs" /> <Compile Include="MQTT\HomeAssistant\Switch.cs" />
<Compile Include="MQTT\Light.cs" /> <Compile Include="MQTT\HomeAssistant\Light.cs" />
<Compile Include="MQTT\MappingExtensions.cs" /> <Compile Include="MQTT\MappingExtensions.cs" />
<Compile Include="MQTT\Sensor.cs" /> <Compile Include="MQTT\HomeAssistant\Sensor.cs" />
<Compile Include="MQTT\Topic.cs" /> <Compile Include="MQTT\Parser\Topic.cs" />
<Compile Include="MQTT\UnitCommands.cs" /> <Compile Include="MQTT\Parser\UnitCommands.cs" />
<Compile Include="MQTT\ZoneCommands.cs" /> <Compile Include="MQTT\Parser\ZoneCommands.cs" />
<Compile Include="MQTT\UnitType.cs" />
<Compile Include="Notifications\EmailNotification.cs" /> <Compile Include="Notifications\EmailNotification.cs" />
<Compile Include="Notifications\INotification.cs" /> <Compile Include="Notifications\INotification.cs" />
<Compile Include="Notifications\Notification.cs" /> <Compile Include="Notifications\Notification.cs" />

View file

@ -4,4 +4,7 @@
<StartArguments> <StartArguments>
</StartArguments> </StartArguments>
</PropertyGroup> </PropertyGroup>
<PropertyGroup>
<ProjectView>ProjectFiles</ProjectView>
</PropertyGroup>
</Project> </Project>

View file

@ -49,13 +49,24 @@ mqtt_prefix = omnilink
mqtt_discovery_prefix = homeassistant mqtt_discovery_prefix = homeassistant
# Prefix for Home Assistant entity names # Prefix for Home Assistant entity names
mqtt_discovery_name_prefix = mqtt_discovery_name_prefix =
# Specify a range of numbers like 1,2,3,5-10 # Skip publishing Home Assistant discovery topics for zones/units
# Specify a range of numbers 1,2,3,5-10
mqtt_discovery_ignore_zones = mqtt_discovery_ignore_zones =
mqtt_discovery_ignore_units = 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 = mqtt_discovery_area_code_required =
# device_class must be battery, door, garage_door, gas, moisture, motion, problem, smoke, or window # 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=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
# 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
# Notifications (yes/no) # Notifications (yes/no)
# Always sent for area alarms and critical system events # Always sent for area alarms and critical system events

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.13.0")] [assembly: AssemblyVersion("1.1.14.0")]
[assembly: AssemblyFileVersion("1.1.13.0")] [assembly: AssemblyFileVersion("1.1.14.0")]

View file

@ -7,7 +7,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Net.Mail; using System.Net.Mail;
using System.Reflection; using System.Reflection;
using System.Threading; using ha = OmniLinkBridge.MQTT.HomeAssistant;
namespace OmniLinkBridge namespace OmniLinkBridge
{ {
@ -86,6 +86,7 @@ namespace OmniLinkBridge
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_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");
Global.mqtt_discovery_override_unit = settings.LoadOverrideUnit<MQTT.OverrideUnit>("mqtt_discovery_override_unit");
} }
// Notifications // Notifications
@ -157,7 +158,7 @@ namespace OmniLinkBridge
} }
else if (override_zone is MQTT.OverrideZone mqtt_zone) else if (override_zone is MQTT.OverrideZone mqtt_zone)
{ {
if (!attributes.ContainsKey("device_class") || !Enum.TryParse(attributes["device_class"], out MQTT.BinarySensor.DeviceClass attrib_device_class)) if (!attributes.ContainsKey("device_class") || !Enum.TryParse(attributes["device_class"], out ha.BinarySensor.DeviceClass attrib_device_class))
throw new Exception("Missing or invalid device_class attribute"); throw new Exception("Missing or invalid device_class attribute");
mqtt_zone.device_class = attrib_device_class; mqtt_zone.device_class = attrib_device_class;
@ -175,6 +176,50 @@ namespace OmniLinkBridge
} }
} }
private static ConcurrentDictionary<int, T> LoadOverrideUnit<T>(this NameValueCollection settings, string section) where T : new()
{
try
{
ConcurrentDictionary<int, T> ret = new ConcurrentDictionary<int, T>();
string value = settings.CheckEnv(section);
if (string.IsNullOrEmpty(value))
return ret;
string[] ids = value.Split(',');
for (int i = 0; i < ids.Length; i++)
{
Dictionary<string, string> 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_unit = new T();
if (override_unit is MQTT.OverrideUnit mqtt_unit)
{
if (!attributes.ContainsKey("type") || !Enum.TryParse(attributes["type"], out MQTT.UnitType attrib_type))
throw new Exception("Missing or invalid type attribute");
mqtt_unit.type = attrib_type;
}
ret.TryAdd(attrib_id, override_unit);
}
return ret;
}
catch (Exception ex)
{
log.Error(ex, "Invalid override unit specified for {section}", section);
throw;
}
}
private static string ValidateHasValue(this NameValueCollection settings, string section) private static string ValidateHasValue(this NameValueCollection settings, string section)
{ {
string value = settings.CheckEnv(section); string value = settings.CheckEnv(section);

View file

@ -1,9 +1,7 @@
using System; using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Text;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OmniLinkBridge; using OmniLinkBridge;
using OmniLinkBridge.MQTT; using OmniLinkBridge.MQTT;
using System.Collections.Generic;
namespace OmniLinkBridgeTest namespace OmniLinkBridgeTest
{ {

View file

@ -1,10 +1,9 @@
using System; using HAI_Shared;
using System.Text;
using System.Collections.Generic;
using HAI_Shared;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using OmniLinkBridge;
using OmniLinkBridge.MQTT; using OmniLinkBridge.MQTT;
using OmniLinkBridgeTest.Mock; using OmniLinkBridgeTest.Mock;
using System.Collections.Concurrent;
namespace OmniLinkBridgeTest namespace OmniLinkBridgeTest
{ {
@ -19,6 +18,8 @@ namespace OmniLinkBridgeTest
{ {
omniLink = new MockOmniLinkII(); omniLink = new MockOmniLinkII();
messageProcessor = new MessageProcessor(omniLink); messageProcessor = new MessageProcessor(omniLink);
omniLink.Controller.Units[395].Type = enuOL2UnitType.Flag;
} }
[TestMethod] [TestMethod]
@ -134,6 +135,28 @@ namespace OmniLinkBridgeTest
check(2, "on", enuUnitCommand.On); check(2, "on", enuUnitCommand.On);
} }
[TestMethod]
public void UnitFlagCommand()
{
void check(ushort id, string payload, enuUnitCommand command, int value)
{
SendCommandEventArgs actual = null;
omniLink.OnSendCommand += (sender, e) => { actual = e; };
messageProcessor.Process($"omnilink/unit{id}/flag_command", payload);
SendCommandEventArgs expected = new SendCommandEventArgs()
{
Cmd = command,
Par = (byte)value,
Pr2 = id
};
Assert.AreEqual(expected, actual);
}
check(395, "0", enuUnitCommand.Set, 0);
check(395, "1", enuUnitCommand.Set, 1);
check(395, "255", enuUnitCommand.Set, 255);
}
[TestMethod] [TestMethod]
public void UnitLevelCommand() public void UnitLevelCommand()
{ {

View file

@ -1,10 +1,6 @@
using HAI_Shared; using HAI_Shared;
using OmniLinkBridge.OmniLink; using OmniLinkBridge.OmniLink;
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OmniLinkBridgeTest.Mock namespace OmniLinkBridgeTest.Mock
{ {

View file

@ -1,9 +1,5 @@
using HAI_Shared; using HAI_Shared;
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OmniLinkBridgeTest.Mock namespace OmniLinkBridgeTest.Mock
{ {

View file

@ -1,9 +1,6 @@
using System; using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Text;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OmniLinkBridge.Notifications;
using OmniLinkBridge; using OmniLinkBridge;
using OmniLinkBridge.Notifications;
using System.Net.Mail; using System.Net.Mail;
namespace OmniLinkBridgeTest namespace OmniLinkBridgeTest

View file

@ -1,9 +1,8 @@
using System; using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Text;
using System.Collections.Generic;
using System.Collections.Concurrent;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OmniLinkBridge; using OmniLinkBridge;
using System;
using System.Collections.Generic;
using ha = OmniLinkBridge.MQTT.HomeAssistant;
namespace OmniLinkBridgeTest namespace OmniLinkBridgeTest
{ {
@ -156,8 +155,11 @@ namespace OmniLinkBridgeTest
"mqtt_discovery_name_prefix = mynameprefix", "mqtt_discovery_name_prefix = mynameprefix",
"mqtt_discovery_ignore_zones = 1,2-3,4", "mqtt_discovery_ignore_zones = 1,2-3,4",
"mqtt_discovery_ignore_units = 2-5,7", "mqtt_discovery_ignore_units = 2-5,7",
"mqtt_discovery_area_code_required = 1",
"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=7;device_class=motion", "mqtt_discovery_override_zone = id=7;device_class=motion",
"mqtt_discovery_override_unit = id=1;type=switch",
"mqtt_discovery_override_unit = id=395;type=light",
}); });
Settings.LoadSettings(lines.ToArray()); Settings.LoadSettings(lines.ToArray());
Assert.AreEqual("myuser", Global.mqtt_username); Assert.AreEqual("myuser", Global.mqtt_username);
@ -167,11 +169,12 @@ namespace OmniLinkBridgeTest
Assert.AreEqual("mynameprefix ", Global.mqtt_discovery_name_prefix); 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_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_ignore_units.SetEquals(new int[] { 2, 3, 4, 5, 7 }));
Assert.IsTrue(Global.mqtt_discovery_area_code_required.SetEquals(new int[] { 1 }));
Dictionary<int, OmniLinkBridge.MQTT.OverrideZone> override_zone = new Dictionary<int, OmniLinkBridge.MQTT.OverrideZone>() Dictionary<int, OmniLinkBridge.MQTT.OverrideZone> override_zone = new Dictionary<int, OmniLinkBridge.MQTT.OverrideZone>()
{ {
{ 5, new OmniLinkBridge.MQTT.OverrideZone { device_class = OmniLinkBridge.MQTT.BinarySensor.DeviceClass.garage_door }}, { 5, new OmniLinkBridge.MQTT.OverrideZone { device_class = ha.BinarySensor.DeviceClass.garage_door }},
{ 7, new OmniLinkBridge.MQTT.OverrideZone { device_class = OmniLinkBridge.MQTT.BinarySensor.DeviceClass.motion }} { 7, new OmniLinkBridge.MQTT.OverrideZone { device_class = ha.BinarySensor.DeviceClass.motion }}
}; };
Assert.AreEqual(override_zone.Count, Global.mqtt_discovery_override_zone.Count); Assert.AreEqual(override_zone.Count, Global.mqtt_discovery_override_zone.Count);
@ -180,6 +183,19 @@ namespace OmniLinkBridgeTest
Global.mqtt_discovery_override_zone.TryGetValue(pair.Key, out OmniLinkBridge.MQTT.OverrideZone value); Global.mqtt_discovery_override_zone.TryGetValue(pair.Key, out OmniLinkBridge.MQTT.OverrideZone value);
Assert.AreEqual(override_zone[pair.Key].device_class, value.device_class); Assert.AreEqual(override_zone[pair.Key].device_class, value.device_class);
} }
Dictionary<int, OmniLinkBridge.MQTT.OverrideUnit> override_unit = new Dictionary<int, OmniLinkBridge.MQTT.OverrideUnit>()
{
{ 1, new OmniLinkBridge.MQTT.OverrideUnit { type = OmniLinkBridge.MQTT.UnitType.@switch }},
{ 395, new OmniLinkBridge.MQTT.OverrideUnit { type = OmniLinkBridge.MQTT.UnitType.light }}
};
Assert.AreEqual(override_unit.Count, Global.mqtt_discovery_override_unit.Count);
foreach (KeyValuePair<int, OmniLinkBridge.MQTT.OverrideUnit> pair in override_unit)
{
Global.mqtt_discovery_override_unit.TryGetValue(pair.Key, out OmniLinkBridge.MQTT.OverrideUnit value);
Assert.AreEqual(override_unit[pair.Key].type, value.type);
}
} }
[TestMethod] [TestMethod]

View file

@ -9,7 +9,7 @@ You can use docker to build an image from git or download the [binary here](http
- .NET Framework 4.7.2 (or Mono equivalent) - .NET Framework 4.7.2 (or Mono equivalent)
## Operation ## Operation
OmniLink Bridge is divided into the following modules and configurable settings. Configuration settings can also be set as environment variables by using their name in uppercase. Refer to [OmniLinkBridge.ini](https://github.com/excaliburpartners/OmniLinkBridge/blob/master/OmniLinkBridge/OmniLinkBridge.ini) for specifics. OmniLink Bridge is divided into the following modules and configurable settings. Configuration settings can also be set as environment variables by using their name in uppercase. Refer to [OmniLinkBridge.ini](OmniLinkBridge/OmniLinkBridge.ini) for specifics.
- OmniLinkII: controller_ - OmniLinkII: controller_
- Maintains connection to the OmniLink controller - Maintains connection to the OmniLink controller
@ -232,6 +232,10 @@ SUB omnilink/unitX/brightness_state
PUB omnilink/unitX/brightness_command PUB omnilink/unitX/brightness_command
int Level from 0 to 100 percent int Level from 0 to 100 percent
SUB omnilink/unitX/flag_state
PUB omnilink/unitX/flag_command
int Level from 0 to 255
SUB omnilink/unitX/scene_state SUB omnilink/unitX/scene_state
PUB omnilink/unitX/scene_command PUB omnilink/unitX/scene_command
string A-L string A-L