Compare commits

..

18 Commits

Author SHA1 Message Date
Ryan Wagoner
efa16fb2c3 1.1.19 - Fix MQTT alarm control panel for Home Assistant 2024.6 2024-06-07 21:39:43 -04:00
Ryan Wagoner
f297c2fcaa 1.1.18 - Increase timeout for loading names and add send support logs 2024-05-08 20:25:31 -04:00
Ryan Wagoner
d7eef51adf 1.1.17 - Add MQTT volume scaling for Home Assistant media player 2024-05-05 15:36:00 -04:00
Ryan Wagoner
73f504ca93 1.1.16 - Add MQTT audio support 2024-05-03 21:35:46 -04:00
Ryan Wagoner
800242a87f 1.1.15 - Add notice for deprecated modules and update packages 2024-04-13 02:21:22 -04:00
Ryan Wagoner
84b52c8f30 Add MQTT lock support 2024-04-12 00:00:26 -04:00
Ryan Wagoner
495ce74149 1.1.14 - Improve MQTT unit, output and flag capabilities 2022-11-02 18:09:33 -04:00
Ryan Wagoner
a016e1cd64 Implement MQTT thermostat offline status 2022-10-20 23:44:27 -04:00
Ryan Wagoner
1ce5e3dab9 1.1.13 - Add MQTT area security code support 2022-10-20 21:43:39 -04:00
Ryan Wagoner
7c24d9046e Add support to build docker for multiple architectures 2022-07-13 18:28:21 -04:00
Ryan Wagoner
41330b9bf4 1.1.12 - MQTT area basic_state now includes arm_vacation to align with changes to Home Assistant 2022-04-04 23:11:01 -04:00
Ryan Wagoner
3b86dd6a3a Update MQTT Home Assistant discovery to use preset mode instead of deprecated hold mode 2022-04-04 23:10:41 -04:00
Ryan Wagoner
5cd6048dd5 Fix system trouble naming and discovery payloads 2021-12-16 23:33:42 -05:00
Ryan Wagoner
b5298a1e74 Update to NET Framework 4.7.2 and cleanup warnings 2021-12-15 20:04:26 -05:00
Ryan Wagoner
e7613741d3 Fix controller reconnect issue and async cleanup 2021-12-14 01:13:43 -05:00
Ryan Wagoner
2f652f7c8a 1.1.11 - Add MQTT thermostat humidify and dehumidify discovery 2021-11-04 21:49:14 -04:00
Ryan Wagoner
a8d965eb04 Add thermostat emergency heat and fix hold documentation 2021-11-04 20:34:13 -04:00
Ryan Wagoner
a248bd4f30 - Add MQTT area and zone user code support and update packages 2021-07-28 20:33:47 -04:00
70 changed files with 2176 additions and 495 deletions

View File

@ -1,14 +1,22 @@
FROM mono:latest AS build
ARG TARGETPLATFORM
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
RUN apt-get update && \
apt-get install -y unixodbc
WORKDIR /build
ADD https://dev.mysql.com/get/Downloads/Connector-ODBC/8.0/mysql-connector-odbc-8.0.18-linux-debian9-x86-64bit.tar.gz /build
RUN tar zxf mysql-connector-odbc-8.0.18-linux-debian9-x86-64bit.tar.gz && \
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
tar zxf mysql-connector-odbc-8.0.18-linux-debian9-x86-64bit.tar.gz && \
mkdir -p /usr/lib/odbc/ && \
cp mysql-connector-odbc-8.0.18-linux-debian9-x86-64bit/lib/* /usr/lib/odbc/ && \
mysql-connector-odbc-8.0.18-linux-debian9-x86-64bit/bin/myodbc-installer -d -a -n "MySQL" -t "DRIVER=/usr/lib/odbc/libmyodbc8w.so"
mysql-connector-odbc-8.0.18-linux-debian9-x86-64bit/bin/myodbc-installer -d -a -n "MySQL" -t "DRIVER=/usr/lib/odbc/libmyodbc8w.so"; \
else \
mkdir -p /usr/lib/odbc/ && \
touch /etc/odbcinst.ini; \
fi
COPY . .
RUN nuget restore /build/OmniLinkBridge.sln

View File

@ -85,9 +85,9 @@ CREATE TABLE IF NOT EXISTS `log_thermostats` (
`humidity` smallint(6) NOT NULL,
`humidify` smallint(6) NOT NULL,
`dehumidify` smallint(6) NOT NULL,
`mode` varchar(5) NOT NULL,
`mode` varchar(14) NOT NULL,
`fan` varchar(5) NOT NULL,
`hold` varchar(5) NOT NULL,
`hold` varchar(8) NOT NULL,
PRIMARY KEY (`log_tstat_id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2"/>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2"/>
</startup>
</configuration>

View File

@ -0,0 +1,16 @@
using System;
using Serilog.Core;
using Serilog.Events;
namespace OmniLinkBridge
{
public class ControllerEnricher : ILogEventEnricher
{
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
if(Global.controller_id != Guid.Empty)
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(
"ControllerId", Global.controller_id));
}
}
}

View File

@ -44,9 +44,11 @@ namespace OmniLinkBridge
startTime = DateTime.Now;
Program.ShowSendLogsWarning();
using (LogContext.PushProperty("Telemetry", "Startup"))
log.Information("Started version {Version} on {OperatingSystem} with {Modules}",
Assembly.GetExecutingAssembly().GetName().Version, Environment.OSVersion, modules);
log.Information("Started version {Version} in {Environment} on {OperatingSystem} with {Modules}",
Assembly.GetExecutingAssembly().GetName().Version, Program.GetEnvironment(), Environment.OSVersion, modules);
// Startup modules
foreach (IModule module in modules)

View File

@ -1,12 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
namespace OmniLinkBridge
{
public static class Extensions
{
public static string Truncate(this string value, int maxLength)
{
return value?.Length > maxLength ? value.Substring(0, maxLength) : value;
}
public static double ToCelsius(this double f)
{
// Convert to celsius
@ -23,7 +30,6 @@ namespace OmniLinkBridge
{
return (b & (1 << pos)) != 0;
}
public static string ToSpaceTitleCase(this string phrase)
{
return Regex.Replace(phrase, "(\\B[A-Z])", " $1");
@ -44,5 +50,14 @@ namespace OmniLinkBridge
.Select(t => int.Parse(t)).ToList(); // digit to int
return RangeNums.Count.Equals(2) ? Enumerable.Range(RangeNums.Min(), (RangeNums.Max() + 1) - RangeNums.Min()).ToList() : RangeNums;
}
public static Guid ComputeGuid(this string data)
{
using SHA256 hash = SHA256.Create();
byte[] bytes = hash.ComputeHash(Encoding.UTF8.GetBytes(data));
byte[] guidBytes = new byte[16];
Array.Copy(bytes, guidBytes, 16);
return new Guid(guidBytes);
}
}
}

View File

@ -1,3 +1,4 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net.Mail;
@ -9,6 +10,8 @@ namespace OmniLinkBridge
{
public static bool DebugSettings { get; set; }
public static bool UseEnvironment { get; set; }
public static bool SendLogs { get; set; }
public static Guid SessionID { get; } = Guid.NewGuid();
// HAI / Leviton Omni Controller
public static string controller_address;
@ -16,6 +19,7 @@ namespace OmniLinkBridge
public static string controller_key1;
public static string controller_key2;
public static string controller_name;
public static Guid controller_id;
// Time Sync
public static bool time_sync;
@ -31,6 +35,8 @@ namespace OmniLinkBridge
public static bool verbose_thermostat;
public static bool verbose_unit;
public static bool verbose_message;
public static bool verbose_lock;
public static bool verbose_audio;
// mySQL Logging
public static bool mysql_logging;
@ -53,7 +59,12 @@ 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 ConcurrentDictionary<int, MQTT.OverrideArea> mqtt_discovery_override_area;
public static ConcurrentDictionary<int, MQTT.OverrideZone> mqtt_discovery_override_zone;
public static ConcurrentDictionary<int, MQTT.OverrideUnit> mqtt_discovery_override_unit;
public static Type mqtt_discovery_button_type;
public static bool mqtt_audio_local_mute;
public static bool mqtt_audio_volume_media_player;
// Notifications
public static bool notify_area;

View File

@ -1,9 +0,0 @@
namespace OmniLinkBridge.MQTT
{
public class Alarm : Device
{
public string command_topic { get; set; }
//public string code { get; set; } = string.Empty;
}
}

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

@ -0,0 +1,7 @@
namespace OmniLinkBridge.MQTT
{
public class Availability
{
public string topic { get; set; } = $"{Global.mqtt_prefix}/status";
}
}

View File

@ -1,20 +0,0 @@
using Newtonsoft.Json;
using OmniLinkBridge.Modules;
namespace OmniLinkBridge.MQTT
{
public class Device
{
public string unique_id { get; set; }
public string name { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string state_topic { get; set; }
public string availability_topic { get; set; } = $"{Global.mqtt_prefix}/status";
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public DeviceRegistry device { get; set; } = MQTTModule.MqttDeviceRegistry;
}
}

View File

@ -0,0 +1,66 @@
using HAI_Shared;
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)
{
// Special case for Home Assistant when code not required
if (string.Compare(payloads[1], "validate", true) == 0 &&
string.Compare(payloads[2], "None", true) == 0)
{
ret.Success = true;
}
else 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

@ -0,0 +1,30 @@
using Newtonsoft.Json;
using System.Collections.Generic;
namespace OmniLinkBridge.MQTT.HomeAssistant
{
public class Alarm : Device
{
public Alarm(DeviceRegistry deviceRegistry) : base(deviceRegistry)
{
}
public string command_topic { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string command_template { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string code { get; set; }
public bool code_arm_required { get; set; } = false;
public bool code_disarm_required { get; set; } = false;
public bool code_trigger_required { get; set; } = false;
public List<string> supported_features { get; set; } = new List<string>(new string[] {
"arm_home", "arm_away", "arm_night", "arm_vacation" });
}
}

View File

@ -1,10 +1,15 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace OmniLinkBridge.MQTT
namespace OmniLinkBridge.MQTT.HomeAssistant
{
public class BinarySensor : Device
{
public BinarySensor(DeviceRegistry deviceRegistry) : base(deviceRegistry)
{
}
[JsonConverter(typeof(StringEnumConverter))]
public enum DeviceClass
{
@ -27,5 +32,11 @@ namespace OmniLinkBridge.MQTT
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string value_template { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string payload_off { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string payload_on { get; set; }
}
}

View File

@ -0,0 +1,17 @@
using Newtonsoft.Json;
namespace OmniLinkBridge.MQTT.HomeAssistant
{
public class Button : Device
{
public Button(DeviceRegistry deviceRegistry) : base(deviceRegistry)
{
}
public string command_topic { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string payload_press { get; set; }
}
}

View File

@ -1,9 +1,16 @@
using System.Collections.Generic;
namespace OmniLinkBridge.MQTT
namespace OmniLinkBridge.MQTT.HomeAssistant
{
public class Climate : Device
{
public Climate(DeviceRegistry deviceRegistry) : base(deviceRegistry)
{
}
public string status { get; set; }
public string action_topic { get; set; }
public string current_temperature_topic { get; set; }
@ -24,7 +31,8 @@ namespace OmniLinkBridge.MQTT
public string fan_mode_command_topic { get; set; }
public List<string> fan_modes { get; set; } = new List<string>(new string[] { "auto", "on", "cycle" });
public string hold_state_topic { get; set; }
public string hold_command_topic { get; set; }
public string preset_mode_state_topic { get; set; }
public string preset_mode_command_topic { get; set; }
public List<string> preset_modes { get; set; } = new List<string>(new string[] { "off", "on", "vacation" });
}
}

View File

@ -0,0 +1,44 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System.Collections.Generic;
namespace OmniLinkBridge.MQTT.HomeAssistant
{
public class Device
{
public Device(DeviceRegistry deviceRegistry)
{
device = deviceRegistry;
}
[JsonConverter(typeof(StringEnumConverter))]
public enum AvailabilityMode
{
all,
any,
latest
}
public string unique_id { get; set; }
public string name { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string icon { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string state_topic { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string availability_topic { get; set; } = $"{Global.mqtt_prefix}/status";
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public List<Availability> availability { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public AvailabilityMode? availability_mode { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public DeviceRegistry device { get; set; }
}
}

View File

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

View File

@ -1,7 +1,12 @@
namespace OmniLinkBridge.MQTT
namespace OmniLinkBridge.MQTT.HomeAssistant
{
public class Light : Device
{
public Light(DeviceRegistry deviceRegistry) : base(deviceRegistry)
{
}
public string command_topic { get; set; }
public string brightness_state_topic { get; set; }

View File

@ -0,0 +1,26 @@
using Newtonsoft.Json;
namespace OmniLinkBridge.MQTT.HomeAssistant
{
public class Lock : Device
{
public Lock(DeviceRegistry deviceRegistry) : base(deviceRegistry)
{
}
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

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

View File

@ -0,0 +1,16 @@
using System.Collections.Generic;
namespace OmniLinkBridge.MQTT.HomeAssistant
{
public class Select : Device
{
public Select(DeviceRegistry deviceRegistry) : base(deviceRegistry)
{
}
public string command_topic { get; set; }
public List<string> options { get; set; } = null;
}
}

View File

@ -1,10 +1,15 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace OmniLinkBridge.MQTT
namespace OmniLinkBridge.MQTT.HomeAssistant
{
public class Sensor : Device
{
public Sensor(DeviceRegistry deviceRegistry) : base(deviceRegistry)
{
}
[JsonConverter(typeof(StringEnumConverter))]
public enum DeviceClass
{
@ -15,9 +20,6 @@ namespace OmniLinkBridge.MQTT
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public DeviceClass? device_class { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string icon { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string unit_of_measurement { get; set; }

View File

@ -1,9 +1,14 @@
using Newtonsoft.Json;
namespace OmniLinkBridge.MQTT
namespace OmniLinkBridge.MQTT.HomeAssistant
{
public class Switch : Device
{
public Switch(DeviceRegistry deviceRegistry) : base(deviceRegistry)
{
}
public string command_topic { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]

View File

@ -1,6 +1,9 @@
using HAI_Shared;
using Newtonsoft.Json;
using System.Collections.Generic;
using OmniLinkBridge.MQTT.HomeAssistant;
using OmniLinkBridge.MQTT.Parser;
using OmniLinkBridge.Modules;
namespace OmniLinkBridge.MQTT
{
@ -13,13 +16,38 @@ namespace OmniLinkBridge.MQTT
public static Alarm ToConfig(this clsArea area)
{
Alarm ret = new Alarm
Alarm ret = new Alarm(MQTTModule.MqttDeviceRegistry)
{
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),
};
Global.mqtt_discovery_override_area.TryGetValue(area.Number, out OverrideArea override_area);
if (override_area != null)
{
if(override_area.code_arm || override_area.code_disarm)
{
ret.command_template = "{{ action }},validate,{{ code }}";
ret.code = "REMOTE_CODE";
}
ret.code_arm_required = override_area.code_arm;
ret.code_disarm_required = override_area.code_disarm;
ret.supported_features.Clear();
if (override_area.arm_home)
ret.supported_features.Add("arm_home");
if (override_area.arm_away)
ret.supported_features.Add("arm_away");
if (override_area.arm_night)
ret.supported_features.Add("arm_night");
if (override_area.arm_vacation)
ret.supported_features.Add("arm_vacation");
}
return ret;
}
@ -62,8 +90,9 @@ namespace OmniLinkBridge.MQTT
case enuSecurityMode.DayInst:
return "armed_home";
case enuSecurityMode.Away:
case enuSecurityMode.Vacation:
return "armed_away";
case enuSecurityMode.Vacation:
return "armed_vacation";
case enuSecurityMode.Off:
default:
return "disarmed";
@ -72,7 +101,7 @@ namespace OmniLinkBridge.MQTT
public static BinarySensor ToConfigBurglary(this clsArea area)
{
BinarySensor ret = new BinarySensor
BinarySensor ret = new BinarySensor(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}area{area.Number}burglary",
name = $"{Global.mqtt_discovery_name_prefix}{area.Name} Burglary",
@ -85,7 +114,7 @@ namespace OmniLinkBridge.MQTT
public static BinarySensor ToConfigFire(this clsArea area)
{
BinarySensor ret = new BinarySensor
BinarySensor ret = new BinarySensor(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}area{area.Number}fire",
name = $"{Global.mqtt_discovery_name_prefix}{area.Name} Fire",
@ -98,7 +127,7 @@ namespace OmniLinkBridge.MQTT
public static BinarySensor ToConfigGas(this clsArea area)
{
BinarySensor ret = new BinarySensor
BinarySensor ret = new BinarySensor(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}area{area.Number}gas",
name = $"{Global.mqtt_discovery_name_prefix}{area.Name} Gas",
@ -111,20 +140,20 @@ namespace OmniLinkBridge.MQTT
public static BinarySensor ToConfigAux(this clsArea area)
{
BinarySensor ret = new BinarySensor
BinarySensor ret = new BinarySensor(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}area{area.Number}auxiliary",
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;
}
public static BinarySensor ToConfigFreeze(this clsArea area)
{
BinarySensor ret = new BinarySensor
BinarySensor ret = new BinarySensor(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}area{area.Number}freeze",
name = $"{Global.mqtt_discovery_name_prefix}{area.Name} Freeze",
@ -137,7 +166,7 @@ namespace OmniLinkBridge.MQTT
public static BinarySensor ToConfigWater(this clsArea area)
{
BinarySensor ret = new BinarySensor
BinarySensor ret = new BinarySensor(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}area{area.Number}water",
name = $"{Global.mqtt_discovery_name_prefix}{area.Name} Water",
@ -150,7 +179,7 @@ namespace OmniLinkBridge.MQTT
public static BinarySensor ToConfigDuress(this clsArea area)
{
BinarySensor ret = new BinarySensor
BinarySensor ret = new BinarySensor(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}area{area.Number}duress",
name = $"{Global.mqtt_discovery_name_prefix}{area.Name} Duress",
@ -163,7 +192,7 @@ namespace OmniLinkBridge.MQTT
public static BinarySensor ToConfigTemp(this clsArea area)
{
BinarySensor ret = new BinarySensor
BinarySensor ret = new BinarySensor(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}area{area.Number}temp",
name = $"{Global.mqtt_discovery_name_prefix}{area.Name} Temp",
@ -176,7 +205,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),
@ -186,18 +215,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);
}
@ -209,7 +237,7 @@ namespace OmniLinkBridge.MQTT
public static Sensor ToConfigTemp(this clsZone zone, enuTempFormat format)
{
Sensor ret = new Sensor
Sensor ret = new Sensor(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}zone{zone.Number}temp",
name = $"{Global.mqtt_discovery_name_prefix}{zone.Name} Temp",
@ -222,7 +250,7 @@ namespace OmniLinkBridge.MQTT
public static Sensor ToConfigHumidity(this clsZone zone)
{
Sensor ret = new Sensor
Sensor ret = new Sensor(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}zone{zone.Number}humidity",
name = $"{Global.mqtt_discovery_name_prefix}{zone.Name} Humidity",
@ -235,7 +263,7 @@ namespace OmniLinkBridge.MQTT
public static Sensor ToConfigSensor(this clsZone zone)
{
Sensor ret = new Sensor
Sensor ret = new Sensor(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}zone{zone.Number}",
name = Global.mqtt_discovery_name_prefix + zone.Name
@ -277,7 +305,7 @@ namespace OmniLinkBridge.MQTT
public static Switch ToConfigSwitch(this clsZone zone)
{
Switch ret = new Switch
Switch ret = new Switch(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}zone{zone.Number}switch",
name = $"{Global.mqtt_discovery_name_prefix}{zone.Name} Bypass",
@ -292,7 +320,7 @@ namespace OmniLinkBridge.MQTT
public static BinarySensor ToConfig(this clsZone zone)
{
BinarySensor ret = new BinarySensor
BinarySensor ret = new BinarySensor(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}zone{zone.Number}binary",
name = Global.mqtt_discovery_name_prefix + zone.Name
@ -367,7 +395,7 @@ namespace OmniLinkBridge.MQTT
public static Light ToConfig(this clsUnit unit)
{
Light ret = new Light
Light ret = new Light(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}unit{unit.Number}light",
name = Global.mqtt_discovery_name_prefix + unit.Name,
@ -381,7 +409,7 @@ namespace OmniLinkBridge.MQTT
public static Switch ToConfigSwitch(this clsUnit unit)
{
Switch ret = new Switch
Switch ret = new Switch(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}unit{unit.Number}switch",
name = Global.mqtt_discovery_name_prefix + unit.Name,
@ -391,6 +419,20 @@ namespace OmniLinkBridge.MQTT
return ret;
}
public static Number ToConfigNumber(this clsUnit unit)
{
Number ret = new Number(MQTTModule.MqttDeviceRegistry)
{
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)
{
return unit.Status == 0 || unit.Status == 100 ? UnitCommands.OFF.ToString() : UnitCommands.ON.ToString();
@ -422,7 +464,7 @@ namespace OmniLinkBridge.MQTT
public static Sensor ToConfigTemp(this clsThermostat thermostat, enuTempFormat format)
{
Sensor ret = new Sensor
Sensor ret = new Sensor(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}thermostat{thermostat.Number}temp",
name = $"{Global.mqtt_discovery_name_prefix}{thermostat.Name} Temp",
@ -433,9 +475,35 @@ namespace OmniLinkBridge.MQTT
return ret;
}
public static Number ToConfigHumidify(this clsThermostat thermostat)
{
Number ret = new Number(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}thermostat{thermostat.Number}humidify",
name = $"{Global.mqtt_discovery_name_prefix}{thermostat.Name} Humidify",
icon = "mdi:water-percent",
state_topic = thermostat.ToTopic(Topic.humidify_state),
command_topic = thermostat.ToTopic(Topic.humidify_command),
};
return ret;
}
public static Number ToConfigDehumidify(this clsThermostat thermostat)
{
Number ret = new Number(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}thermostat{thermostat.Number}dehumidify",
name = $"{Global.mqtt_discovery_name_prefix}{thermostat.Name} Dehumidify",
icon = "mdi:water-percent",
state_topic = thermostat.ToTopic(Topic.dehumidify_state),
command_topic = thermostat.ToTopic(Topic.dehumidify_command),
};
return ret;
}
public static Sensor ToConfigHumidity(this clsThermostat thermostat)
{
Sensor ret = new Sensor
Sensor ret = new Sensor(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}thermostat{thermostat.Number}humidity",
name = $"{Global.mqtt_discovery_name_prefix}{thermostat.Name} Humidity",
@ -448,8 +516,19 @@ namespace OmniLinkBridge.MQTT
public static Climate ToConfig(this clsThermostat thermostat, enuTempFormat format)
{
Climate ret = new Climate
Climate ret = new Climate(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}thermostat{thermostat.Number}",
name = Global.mqtt_discovery_name_prefix + thermostat.Name,
availability_topic = null,
availability_mode = Device.AvailabilityMode.all,
availability = new List<Availability>()
{
new Availability(),
new Availability() { topic = thermostat.ToTopic(Topic.status) }
},
modes = thermostat.Type switch
{
enuThermostatType.AutoHeatCool => new List<string>(new string[] { "auto", "off", "cool", "heat" }),
@ -457,7 +536,25 @@ namespace OmniLinkBridge.MQTT
enuThermostatType.HeatOnly => new List<string>(new string[] { "off", "heat" }),
enuThermostatType.CoolOnly => new List<string>(new string[] { "off", "cool" }),
_ => new List<string>(new string[] { "off" }),
}
},
action_topic = thermostat.ToTopic(Topic.current_operation),
current_temperature_topic = thermostat.ToTopic(Topic.current_temperature),
temperature_low_state_topic = thermostat.ToTopic(Topic.temperature_heat_state),
temperature_low_command_topic = thermostat.ToTopic(Topic.temperature_heat_command),
temperature_high_state_topic = thermostat.ToTopic(Topic.temperature_cool_state),
temperature_high_command_topic = thermostat.ToTopic(Topic.temperature_cool_command),
mode_state_topic = thermostat.ToTopic(Topic.mode_basic_state),
mode_command_topic = thermostat.ToTopic(Topic.mode_command),
fan_mode_state_topic = thermostat.ToTopic(Topic.fan_mode_state),
fan_mode_command_topic = thermostat.ToTopic(Topic.fan_mode_command),
preset_mode_state_topic = thermostat.ToTopic(Topic.hold_state),
preset_mode_command_topic = thermostat.ToTopic(Topic.hold_command)
};
if (format == enuTempFormat.Celsius)
@ -466,26 +563,6 @@ namespace OmniLinkBridge.MQTT
ret.max_temp = "35";
}
ret.unique_id = $"{Global.mqtt_prefix}thermostat{thermostat.Number}";
ret.name = Global.mqtt_discovery_name_prefix + thermostat.Name;
ret.action_topic = thermostat.ToTopic(Topic.current_operation);
ret.current_temperature_topic = thermostat.ToTopic(Topic.current_temperature);
ret.temperature_low_state_topic = thermostat.ToTopic(Topic.temperature_heat_state);
ret.temperature_low_command_topic = thermostat.ToTopic(Topic.temperature_heat_command);
ret.temperature_high_state_topic = thermostat.ToTopic(Topic.temperature_cool_state);
ret.temperature_high_command_topic = thermostat.ToTopic(Topic.temperature_cool_command);
ret.mode_state_topic = thermostat.ToTopic(Topic.mode_state);
ret.mode_command_topic = thermostat.ToTopic(Topic.mode_command);
ret.fan_mode_state_topic = thermostat.ToTopic(Topic.fan_mode_state);
ret.fan_mode_command_topic = thermostat.ToTopic(Topic.fan_mode_command);
ret.hold_state_topic = thermostat.ToTopic(Topic.hold_state);
ret.hold_command_topic = thermostat.ToTopic(Topic.hold_command);
return ret;
}
@ -499,14 +576,30 @@ namespace OmniLinkBridge.MQTT
return "idle";
}
public static string ToModeState(this clsThermostat thermostat)
{
if (thermostat.Mode == enuThermostatMode.E_Heat)
return "e_heat";
else
return thermostat.ModeText().ToLower();
}
public static string ToModeBasicState(this clsThermostat thermostat)
{
if (thermostat.Mode == enuThermostatMode.E_Heat)
return "heat";
else
return thermostat.ModeText().ToLower();
}
public static string ToTopic(this clsButton button, Topic topic)
{
return $"{Global.mqtt_prefix}/button{button.Number}/{topic}";
}
public static Switch ToConfig(this clsButton button)
public static Switch ToConfigSwitch(this clsButton button)
{
Switch ret = new Switch
Switch ret = new Switch(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}button{button.Number}",
name = Global.mqtt_discovery_name_prefix + button.Name,
@ -516,6 +609,18 @@ namespace OmniLinkBridge.MQTT
return ret;
}
public static Button ToConfigButton(this clsButton button)
{
Button ret = new Button(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}button{button.Number}",
name = Global.mqtt_discovery_name_prefix + button.Name,
command_topic = button.ToTopic(Topic.command),
payload_press = "ON"
};
return ret;
}
public static string ToTopic(this clsMessage message, Topic topic)
{
return $"{Global.mqtt_prefix}/message{message.Number}/{topic}";
@ -530,5 +635,135 @@ namespace OmniLinkBridge.MQTT
else
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(MQTTModule.MqttDeviceRegistry)
{
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";
}
public static string ToTopic(this clsAudioSource audioSource, Topic topic)
{
return $"{Global.mqtt_prefix}/source{audioSource.Number}/{topic}";
}
public static string ToTopic(this clsAudioZone audioZone, Topic topic)
{
return $"{Global.mqtt_prefix}/audio{audioZone.Number}/{topic}";
}
public static Switch ToConfig(this clsAudioZone audioZone)
{
Switch ret = new Switch(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}audio{audioZone.Number}",
name = Global.mqtt_discovery_name_prefix + audioZone.rawName,
icon = "mdi:speaker",
state_topic = audioZone.ToTopic(Topic.state),
command_topic = audioZone.ToTopic(Topic.command)
};
return ret;
}
public static string ToState(this clsAudioZone audioZone)
{
return audioZone.Power ? "ON" : "OFF";
}
public static Switch ToConfigMute(this clsAudioZone audioZone)
{
Switch ret = new Switch(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}audio{audioZone.Number}mute",
name = $"{Global.mqtt_discovery_name_prefix}{audioZone.rawName} Mute",
icon = "mdi:volume-mute",
state_topic = audioZone.ToTopic(Topic.mute_state),
command_topic = audioZone.ToTopic(Topic.mute_command)
};
return ret;
}
public static string ToMuteState(this clsAudioZone audioZone)
{
if(Global.mqtt_audio_local_mute)
return audioZone.Volume == 0 ? "ON" : "OFF";
else
return audioZone.Mute ? "ON" : "OFF";
}
public static Select ToConfigSource(this clsAudioZone audioZone, List<string> audioSources)
{
Select ret = new Select(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}audio{audioZone.Number}source",
name = $"{Global.mqtt_discovery_name_prefix}{audioZone.rawName} Source",
icon = "mdi:volume-source",
state_topic = audioZone.ToTopic(Topic.source_state),
command_topic = audioZone.ToTopic(Topic.source_command),
options = audioSources
};
return ret;
}
public static int ToSourceState(this clsAudioZone audioZone)
{
return audioZone.Source;
}
public static Number ToConfigVolume(this clsAudioZone audioZone)
{
Number ret = new Number(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}audio{audioZone.Number}volume",
name = $"{Global.mqtt_discovery_name_prefix}{audioZone.rawName} Volume",
icon = "mdi:volume-low",
state_topic = audioZone.ToTopic(Topic.volume_state),
command_topic = audioZone.ToTopic(Topic.volume_command),
min = 0,
max = 100,
step = 1,
};
if(Global.mqtt_audio_volume_media_player)
{
ret.min = 0;
ret.max = 1;
ret.step = 0.01;
}
return ret;
}
public static double ToVolumeState(this clsAudioZone audioZone)
{
if (Global.mqtt_audio_volume_media_player)
return audioZone.Volume * 0.01;
else
return audioZone.Volume;
}
}
}

View File

@ -1,10 +1,12 @@
using HAI_Shared;
using OmniLinkBridge.MQTT.Parser;
using OmniLinkBridge.OmniLink;
using Serilog;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading;
namespace OmniLinkBridge.MQTT
{
@ -14,11 +16,18 @@ namespace OmniLinkBridge.MQTT
private readonly Regex regexTopic = new Regex(Global.mqtt_prefix + "/([A-Za-z]+)([0-9]+)/(.*)", RegexOptions.Compiled);
private IOmniLinkII OmniLink { get; set; }
private readonly int[] audioMuteVolumes;
private const int VOLUME_DEFAULT = 10;
public MessageProcessor(IOmniLinkII omni)
private IOmniLinkII OmniLink { get; }
private Dictionary<string, int> AudioSources { get; }
public MessageProcessor(IOmniLinkII omni, Dictionary<string, int> audioSources, int numAudioZones)
{
OmniLink = omni;
AudioSources = audioSources;
audioMuteVolumes = new int[numAudioZones];
}
public void Process(string messageTopic, string payload)
@ -28,8 +37,8 @@ namespace OmniLinkBridge.MQTT
if (!match.Success)
return;
if (!Enum.TryParse(match.Groups[1].Value, true, out CommandTypes type)
|| !Enum.TryParse(match.Groups[3].Value, true, out Topic topic)
if (!Enum.TryParse(match.Groups[1].Value, true, out CommandTypes type)
|| !Enum.TryParse(match.Groups[3].Value, true, out Topic topic)
|| !ushort.TryParse(match.Groups[2].Value, out ushort id))
return;
@ -38,7 +47,7 @@ namespace OmniLinkBridge.MQTT
if (type == CommandTypes.area && id <= OmniLink.Controller.Areas.Count)
ProcessAreaReceived(OmniLink.Controller.Areas[id], topic, payload);
else if (type == CommandTypes.zone && id > 0 && id <= OmniLink.Controller.Zones.Count)
else if (type == CommandTypes.zone && id <= OmniLink.Controller.Zones.Count)
ProcessZoneReceived(OmniLink.Controller.Zones[id], topic, payload);
else if (type == CommandTypes.unit && id > 0 && id <= OmniLink.Controller.Units.Count)
ProcessUnitReceived(OmniLink.Controller.Units[id], topic, payload);
@ -48,6 +57,10 @@ namespace OmniLinkBridge.MQTT
ProcessButtonReceived(OmniLink.Controller.Buttons[id], topic, payload);
else if (type == CommandTypes.message && id > 0 && id <= OmniLink.Controller.Messages.Count)
ProcessMessageReceived(OmniLink.Controller.Messages[id], topic, payload);
else if (type == CommandTypes.@lock && id <= OmniLink.Controller.AccessControlReaders.Count)
ProcessLockReceived(OmniLink.Controller.AccessControlReaders[id], topic, payload);
else if (type == CommandTypes.audio && id <= OmniLink.Controller.AudioZones.Count)
ProcessAudioReceived(OmniLink.Controller.AudioZones[id], topic, payload);
}
private static readonly IDictionary<AreaCommands, enuUnitCommand> AreaMapping = new Dictionary<AreaCommands, enuUnitCommand>
@ -56,25 +69,71 @@ namespace OmniLinkBridge.MQTT
{ AreaCommands.arm_home, enuUnitCommand.SecurityDay },
{ AreaCommands.arm_away, enuUnitCommand.SecurityAway },
{ AreaCommands.arm_night, enuUnitCommand.SecurityNight },
{ AreaCommands.arm_vacation, enuUnitCommand.SecurityVac },
// The below aren't supported by Home Assistant
{ AreaCommands.arm_home_instant, enuUnitCommand.SecurityDyi },
{ AreaCommands.arm_night_delay, enuUnitCommand.SecurityNtd },
{ AreaCommands.arm_vacation, enuUnitCommand.SecurityVac },
{ AreaCommands.arm_night_delay, enuUnitCommand.SecurityNtd }
};
private void ProcessAreaReceived(clsArea area, Topic command, string payload)
{
if (command == Topic.command && Enum.TryParse(payload, true, out AreaCommands cmd))
AreaCommandCode parser = payload.ToCommandCode(supportValidate: true);
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], 0, (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)
{
@ -92,10 +151,16 @@ namespace OmniLinkBridge.MQTT
private void ProcessZoneReceived(clsZone zone, Topic command, string payload)
{
if (command == Topic.command && Enum.TryParse(payload, true, out ZoneCommands cmd))
AreaCommandCode parser = payload.ToCommandCode();
if (parser.Success && command == Topic.command && Enum.TryParse(parser.Command, true, out ZoneCommands cmd) &&
!(zone.Number == 0 && cmd == ZoneCommands.bypass))
{
log.Debug("SetZone: {id} to {value}", zone.Number, payload);
OmniLink.SendCommand(ZoneMapping[cmd], 0, (ushort)zone.Number);
if (zone.Number == 0)
log.Debug("SetZone: 0 implies all zones will be restored");
log.Debug("SetZone: {id} to {value}", zone.Number, parser.Command);
OmniLink.SendCommand(ZoneMapping[cmd], (byte)parser.Code, (ushort)zone.Number);
}
}
@ -115,10 +180,16 @@ namespace OmniLinkBridge.MQTT
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);
OmniLink.SendCommand(enuUnitCommand.Level, BitConverter.GetBytes(unitValue)[0], (ushort)unit.Number);
// Force status change instead of waiting on controller to update
@ -126,10 +197,10 @@ namespace OmniLinkBridge.MQTT
// which will cause light to go to 100% brightness
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);
OmniLink.SendCommand(enuUnitCommand.Compose, (byte)(scene - 63), (ushort)unit.Number);
}
}
@ -235,5 +306,111 @@ namespace OmniLinkBridge.MQTT
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);
}
}
private void ProcessAudioReceived(clsAudioZone audioZone, Topic command, string payload)
{
if (command == Topic.command && Enum.TryParse(payload, true, out UnitCommands cmd))
{
if (audioZone.Number == 0)
log.Debug("SetAudio: 0 implies all audio zones will be changed");
log.Debug("SetAudio: {id} to {value}", audioZone.Number, payload);
OmniLink.SendCommand(enuUnitCommand.AudioZone, (byte)cmd, (ushort)audioZone.Number);
// Send power ON twice to workaround Russound standby
if(cmd == UnitCommands.ON)
{
Thread.Sleep(500);
OmniLink.SendCommand(enuUnitCommand.AudioZone, (byte)cmd, (ushort)audioZone.Number);
}
}
else if (command == Topic.mute_command && Enum.TryParse(payload, true, out UnitCommands mute))
{
if (audioZone.Number == 0)
{
if (Global.mqtt_audio_local_mute)
{
log.Warning("SetAudioMute: 0 not supported with local mute");
return;
}
else
log.Debug("SetAudioMute: 0 implies all audio zones will be changed");
}
if (Global.mqtt_audio_local_mute)
{
if (mute == UnitCommands.ON)
{
log.Debug("SetAudioMute: {id} local mute, previous volume {level}",
audioZone.Number, audioZone.Volume);
audioMuteVolumes[audioZone.Number] = audioZone.Volume;
OmniLink.SendCommand(enuUnitCommand.AudioVolume, 0, (ushort)audioZone.Number);
}
else
{
if (audioMuteVolumes[audioZone.Number] == 0)
{
log.Debug("SetAudioMute: {id} local mute, defaulting to volume {level}",
audioZone.Number, VOLUME_DEFAULT);
audioMuteVolumes[audioZone.Number] = VOLUME_DEFAULT;
}
else
{
log.Debug("SetAudioMute: {id} local mute, restoring to volume {level}",
audioZone.Number, audioMuteVolumes[audioZone.Number]);
}
OmniLink.SendCommand(enuUnitCommand.AudioVolume, (byte)audioMuteVolumes[audioZone.Number], (ushort)audioZone.Number);
}
}
else
{
log.Debug("SetAudioMute: {id} to {value}", audioZone.Number, payload);
OmniLink.SendCommand(enuUnitCommand.AudioZone, (byte)(mute + 2), (ushort)audioZone.Number);
}
}
else if (command == Topic.source_command && AudioSources.TryGetValue(payload, out int source))
{
log.Debug("SetAudioSource: {id} to {value}", audioZone.Number, payload);
OmniLink.SendCommand(enuUnitCommand.AudioSource, (byte)source, (ushort)audioZone.Number);
}
else if (command == Topic.volume_command && double.TryParse(payload, out double volume))
{
if (Global.mqtt_audio_volume_media_player)
volume *= 100;
if (volume > 100)
volume = 100;
else if (volume < 0)
volume = 0;
log.Debug("SetAudioVolume: {id} to {value}", audioZone.Number, volume);
OmniLink.SendCommand(enuUnitCommand.AudioVolume, (byte)volume, (ushort)audioZone.Number);
}
}
}
}

View File

@ -0,0 +1,19 @@
using System.Collections.Generic;
namespace OmniLinkBridge.MQTT
{
public class OverrideArea
{
public bool code_arm { get; set; }
public bool code_disarm { get; set; }
public bool arm_home { get; set; } = true;
public bool arm_away { get; set; } = true;
public bool arm_night { get; set; } = true;
public bool arm_vacation { get; set; } = true;
}
}

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
{

View File

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

View File

@ -1,4 +1,4 @@
namespace OmniLinkBridge.MQTT
namespace OmniLinkBridge.MQTT.Parser
{
enum AreaCommands
{
@ -6,9 +6,9 @@
arm_home,
arm_away,
arm_night,
arm_vacation,
// The below aren't supported by Home Assistant
arm_home_instant,
arm_night_delay,
arm_vacation
arm_night_delay
}
}

View File

@ -1,4 +1,4 @@
namespace OmniLinkBridge.MQTT
namespace OmniLinkBridge.MQTT.Parser
{
enum CommandTypes
{
@ -7,6 +7,8 @@
unit,
thermostat,
button,
message
message,
@lock,
audio
}
}

View File

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

View File

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

View File

@ -1,8 +1,9 @@
namespace OmniLinkBridge.MQTT
namespace OmniLinkBridge.MQTT.Parser
{
public enum Topic
{
name,
status,
state,
command,
alarm_command,
@ -10,6 +11,8 @@
json_state,
brightness_state,
brightness_command,
flag_state,
flag_command,
scene_state,
scene_command,
current_operation,
@ -24,10 +27,17 @@
dehumidify_state,
dehumidify_command,
mode_state,
mode_basic_state,
mode_command,
fan_mode_state,
fan_mode_command,
hold_state,
hold_command,
mute_state,
mute_command,
source_state,
source_command,
volume_state,
volume_command,
}
}

View File

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

View File

@ -1,4 +1,4 @@
namespace OmniLinkBridge.MQTT
namespace OmniLinkBridge.MQTT.Parser
{
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.OmniLink;
using Serilog;
using Serilog.Context;
using System;
using System.Collections.Generic;
using System.Data;
@ -38,6 +39,8 @@ namespace OmniLinkBridge.Modules
omnilink.OnThermostatStatus += Omnilink_OnThermostatStatus;
omnilink.OnUnitStatus += Omnilink_OnUnitStatus;
omnilink.OnMessageStatus += Omnilink_OnMessageStatus;
omnilink.OnLockStatus += Omnilink_OnLockStatus;
omnilink.OnAudioZoneStatus += Omnilink_OnAudioZoneStatus;
omnilink.OnSystemStatus += Omnilink_OnSystemStatus;
}
@ -45,6 +48,7 @@ namespace OmniLinkBridge.Modules
{
if (Global.mysql_logging)
{
log.Warning("MySQL logging is deprecated");
log.Information("Connecting to database");
mysql_conn = new OdbcConnection(Global.mysql_connection);
@ -125,15 +129,18 @@ namespace OmniLinkBridge.Modules
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();
if (area.ExitTimer > 0)
@ -146,21 +153,104 @@ 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())
log.Verbose("Initial ZoneStatus {id} {name}, Temp: {temp}", i, zone.Name, zone.TempText());
else
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++;
}
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());
}
ushort audioSourceUsage = 0;
for (ushort i = 1; i <= omnilink.Controller.AudioSources.Count; i++)
{
clsAudioSource audioSource = omnilink.Controller.AudioSources[i];
if (audioSource.DefaultProperties == true)
continue;
audioSourceUsage++;
if (Global.verbose_audio)
log.Verbose("Initial AudioSource {id} {name}", i, audioSource.rawName);
}
ushort audioZoneUsage = 0;
for (ushort i = 1; i <= omnilink.Controller.AudioZones.Count; i++)
{
clsAudioZone audioZone = omnilink.Controller.AudioZones[i];
if (audioZone.DefaultProperties == true)
continue;
audioZoneUsage++;
if (Global.verbose_audio)
log.Verbose("Initial AudioZoneStatus {id} {name}, Power: {power}, Source: {source}, Volume: {volume}, Mute: {mute}",
i, audioZone.rawName, audioZone.Power, audioZone.Source, audioZone.Volume, audioZone.Mute);
}
using (LogContext.PushProperty("Telemetry", "ControllerUsage"))
log.Debug("Controller has {AreaUsage} areas, {ZoneUsage} zones, {UnitUsage} units, " +
"{OutputUsage} outputs, {FlagUsage} flags, {ThermostatUsage} thermostats, {LockUsage} locks, " +
"{AudioSourceUsage} audio sources, {AudioZoneUsage} audio zones",
areaUsage, zoneUsage, unitUsage, outputUsage, flagUsage, thermostatUsage, lockUsage,
audioSourceUsage, audioZoneUsage);
}
private void Omnilink_OnAreaStatus(object sender, AreaStatusEventArgs e)
@ -188,7 +278,7 @@ namespace OmniLinkBridge.Modules
e.Area.AreaDuressAlarmText + "','" + status + "')");
if (Global.verbose_area)
log.Verbose("AreaStatus {id} {name}, Status: {status}, Alarams: {alarms}", e.ID, e.Area.Name, status, e.Area.AreaAlarms);
log.Verbose("AreaStatus {id} {name}, Status: {status}, Alarms: {alarms}", e.ID, e.Area.Name, status, e.Area.AreaAlarms);
if (Global.notify_area && e.Area.LastMode != e.Area.AreaMode)
Notification.Notify("Security", e.Area.Name + " " + e.Area.ModeText());
@ -246,6 +336,10 @@ namespace OmniLinkBridge.Modules
humidity + "','" + humidify + "','" + dehumidify + "','" +
e.Thermostat.ModeText() + "','" + e.Thermostat.FanModeText() + "','" + e.Thermostat.HoldStatusText() + "')");
if (e.Offline)
log.Warning("Unknown temp for Thermostat {thermostatName}, verify thermostat is online",
e.Thermostat.Name);
// Ignore events fired by thermostat polling
if (!e.EventTimer && Global.verbose_thermostat)
log.Verbose("ThermostatStatus {id} {name}, Status: {temp} {status}, " +
@ -291,6 +385,19 @@ namespace OmniLinkBridge.Modules
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_OnAudioZoneStatus(object sender, AudioZoneStatusEventArgs e)
{
if (Global.verbose_audio)
log.Verbose("AudioZoneStatus {id} {name}, Power: {power}, Source: {source}, Volume: {volume}, Mute: {mute}",
e.ID, e.AudioZone.rawName, e.AudioZone.Power, e.AudioZone.Source, e.AudioZone.Volume, e.AudioZone.Mute);
}
private void Omnilink_OnSystemStatus(object sender, SystemStatusEventArgs e)
{
DBQueue(@"

View File

@ -9,10 +9,13 @@ using MQTTnet.Extensions.ManagedClient;
using MQTTnet.Protocol;
using Newtonsoft.Json;
using OmniLinkBridge.MQTT;
using OmniLinkBridge.MQTT.HomeAssistant;
using OmniLinkBridge.MQTT.Parser;
using OmniLinkBridge.OmniLink;
using Serilog;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
@ -31,11 +34,16 @@ namespace OmniLinkBridge.Modules
private bool ControllerConnected { get; set; }
private MessageProcessor MessageProcessor { get; set; }
private Dictionary<string, int> AudioSources { get; set; } = new Dictionary<string, int>();
private readonly AutoResetEvent trigger = new AutoResetEvent(false);
private const string ONLINE = "online";
private const string OFFLINE = "offline";
private const string SECURE = "secure";
private const string TROUBLE = "trouble";
public MQTTModule(OmniLinkII omni)
{
OmniLink = omni;
@ -47,9 +55,11 @@ namespace OmniLinkBridge.Modules
OmniLink.OnThermostatStatus += Omnilink_OnThermostatStatus;
OmniLink.OnButtonStatus += OmniLink_OnButtonStatus;
OmniLink.OnMessageStatus += OmniLink_OnMessageStatus;
OmniLink.OnLockStatus += OmniLink_OnLockStatus;
OmniLink.OnAudioZoneStatus += OmniLink_OnAudioZoneStatus;
OmniLink.OnSystemStatus += OmniLink_OnSystemStatus;
MessageProcessor = new MessageProcessor(omni);
MessageProcessor = new MessageProcessor(omni, AudioSources, omni.Controller.CAP.numAudioZones);
}
public void Startup()
@ -97,7 +107,7 @@ namespace OmniLinkBridge.Modules
MqttClient.ConnectingFailedHandler = new ConnectingFailedHandlerDelegate((e) => log.Error("Error connecting {reason}", e.Exception.Message));
MqttClient.DisconnectedHandler = new MqttClientDisconnectedHandlerDelegate((e) => log.Debug("Disconnected"));
MqttClient.StartAsync(manoptions);
MqttClient.StartAsync(manoptions).Wait();
MqttClient.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate((e) =>
MessageProcessor.Process(e.ApplicationMessage.Topic, Encoding.UTF8.GetString(e.ApplicationMessage.Payload)));
@ -108,6 +118,7 @@ namespace OmniLinkBridge.Modules
Topic.command,
Topic.alarm_command,
Topic.brightness_command,
Topic.flag_command,
Topic.scene_command,
Topic.temperature_heat_command,
Topic.temperature_cool_command,
@ -115,7 +126,10 @@ namespace OmniLinkBridge.Modules
Topic.dehumidify_command,
Topic.mode_command,
Topic.fan_mode_command,
Topic.hold_command
Topic.hold_command,
Topic.mute_command,
Topic.source_command,
Topic.volume_command
};
toSubscribe.ForEach((command) => MqttClient.SubscribeAsync(
@ -126,7 +140,7 @@ namespace OmniLinkBridge.Modules
PublishControllerStatus(OFFLINE);
MqttClient.StopAsync();
MqttClient.StopAsync().Wait();
}
public void Shutdown()
@ -153,7 +167,7 @@ namespace OmniLinkBridge.Modules
private void PublishControllerStatus(string status)
{
log.Information("Publishing controller {status}", status);
PublishAsync($"{Global.mqtt_prefix}/status", status);
PublishAsync($"{Global.mqtt_prefix}/{Topic.status}", status);
}
private void PublishConfig()
@ -165,6 +179,9 @@ namespace OmniLinkBridge.Modules
PublishThermostats();
PublishButtons();
PublishMessages();
PublishLocks();
PublishAudioSources();
PublishAudioZones();
PublishControllerStatus(ONLINE);
PublishAsync($"{Global.mqtt_prefix}/model", OmniLink.Controller.GetModelText());
@ -184,10 +201,10 @@ namespace OmniLinkBridge.Modules
PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/system_dcm/config",
JsonConvert.SerializeObject(SystemTroubleConfig("dcm", "DCM")));
PublishAsync(SystemTroubleTopic("phone"), OmniLink.TroublePhone ? "trouble" : "secure");
PublishAsync(SystemTroubleTopic("ac"), OmniLink.TroubleAC ? "trouble" : "secure");
PublishAsync(SystemTroubleTopic("battery"), OmniLink.TroubleBattery ? "trouble" : "secure");
PublishAsync(SystemTroubleTopic("dcn"), OmniLink.TroubleDCM ? "trouble" : "secure");
PublishAsync(SystemTroubleTopic("phone"), OmniLink.TroublePhone ? TROUBLE : SECURE);
PublishAsync(SystemTroubleTopic("ac"), OmniLink.TroubleAC ? TROUBLE : SECURE);
PublishAsync(SystemTroubleTopic("battery"), OmniLink.TroubleBattery ? TROUBLE : SECURE);
PublishAsync(SystemTroubleTopic("dcm"), OmniLink.TroubleDCM ? TROUBLE : SECURE);
}
public string SystemTroubleTopic(string type)
@ -197,12 +214,14 @@ namespace OmniLinkBridge.Modules
public BinarySensor SystemTroubleConfig(string type, string name)
{
return new BinarySensor
return new BinarySensor(MQTTModule.MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}system{type}",
name = $"{Global.mqtt_discovery_name_prefix} System {name}",
name = $"{Global.mqtt_discovery_name_prefix}System {name}",
state_topic = SystemTroubleTopic(type),
device_class = BinarySensor.DeviceClass.problem
device_class = BinarySensor.DeviceClass.problem,
payload_off = SECURE,
payload_on = TROUBLE
};
}
@ -214,8 +233,8 @@ namespace OmniLinkBridge.Modules
{
clsArea area = OmniLink.Controller.Areas[i];
// PC Access doesn't let you customize the area name for the Omni LTe or Omni IIe
// (configured for 1 area). To workaround ignore default properties for the first area.
// PC Access doesn't let you customize the area name when configured for one area.
// Ignore default properties for the first area.
if (i > 1 && area.DefaultProperties == true)
{
PublishAsync(area.ToTopic(Topic.name), null);
@ -306,6 +325,7 @@ namespace OmniLinkBridge.Modules
for (ushort i = 1; i <= OmniLink.Controller.Units.Count; i++)
{
clsUnit unit = OmniLink.Controller.Units[i];
UnitType unitType = unit.ToUnitType();
if (unit.DefaultProperties == true)
{
@ -319,17 +339,26 @@ namespace OmniLinkBridge.Modules
if (unit.DefaultProperties == true || Global.mqtt_discovery_ignore_units.Contains(unit.Number))
{
string type = i < 385 ? "light" : "switch";
PublishAsync($"{Global.mqtt_discovery_prefix}/{type}/{Global.mqtt_prefix}/unit{i}/config", null);
foreach(UnitType entry in Enum.GetValues(typeof(UnitType)))
PublishAsync($"{Global.mqtt_discovery_prefix}/{entry}/{Global.mqtt_prefix}/unit{i}/config", null);
continue;
}
if (i < 385)
PublishAsync($"{Global.mqtt_discovery_prefix}/light/{Global.mqtt_prefix}/unit{i}/config",
JsonConvert.SerializeObject(unit.ToConfig()));
else
PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/unit{i}/config",
foreach (UnitType entry in Enum.GetValues(typeof(UnitType)).Cast<UnitType>().Where(x => x != unitType))
PublishAsync($"{Global.mqtt_discovery_prefix}/{entry}/{Global.mqtt_prefix}/unit{i}/config", null);
log.Verbose("Publishing {type} {id} {name} as {unitType}", "units", i, unit.Name, unitType);
if (unitType == UnitType.@switch)
PublishAsync($"{Global.mqtt_discovery_prefix}/{unitType}/{Global.mqtt_prefix}/unit{i}/config",
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()));
}
}
@ -345,6 +374,8 @@ namespace OmniLinkBridge.Modules
{
PublishAsync(thermostat.ToTopic(Topic.name), null);
PublishAsync($"{Global.mqtt_discovery_prefix}/climate/{Global.mqtt_prefix}/thermostat{i}/config", null);
PublishAsync($"{Global.mqtt_discovery_prefix}/number/{Global.mqtt_prefix}/thermostat{i}humidify/config", null);
PublishAsync($"{Global.mqtt_discovery_prefix}/number/{Global.mqtt_prefix}/thermostat{i}dehumidify/config", null);
PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/thermostat{i}temp/config", null);
PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/thermostat{i}humidity/config", null);
continue;
@ -353,8 +384,13 @@ namespace OmniLinkBridge.Modules
PublishThermostatState(thermostat);
PublishAsync(thermostat.ToTopic(Topic.name), thermostat.Name);
PublishAsync(thermostat.ToTopic(Topic.status), ONLINE);
PublishAsync($"{Global.mqtt_discovery_prefix}/climate/{Global.mqtt_prefix}/thermostat{i}/config",
JsonConvert.SerializeObject(thermostat.ToConfig(OmniLink.Controller.TempFormat)));
PublishAsync($"{Global.mqtt_discovery_prefix}/number/{Global.mqtt_prefix}/thermostat{i}humidify/config",
JsonConvert.SerializeObject(thermostat.ToConfigHumidify()));
PublishAsync($"{Global.mqtt_discovery_prefix}/number/{Global.mqtt_prefix}/thermostat{i}dehumidify/config",
JsonConvert.SerializeObject(thermostat.ToConfigDehumidify()));
PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/thermostat{i}temp/config",
JsonConvert.SerializeObject(thermostat.ToConfigTemp(OmniLink.Controller.TempFormat)));
PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/thermostat{i}humidity/config",
@ -366,6 +402,9 @@ namespace OmniLinkBridge.Modules
{
log.Debug("Publishing {type}", "buttons");
if (Global.mqtt_discovery_button_type == typeof(Switch))
log.Information("See {setting} for new option when publishing {type}", "mqtt_discovery_button_type", "buttons");
for (ushort i = 1; i <= OmniLink.Controller.Buttons.Count; i++)
{
clsButton button = OmniLink.Controller.Buttons[i];
@ -374,6 +413,7 @@ namespace OmniLinkBridge.Modules
{
PublishAsync(button.ToTopic(Topic.name), null);
PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/button{i}/config", null);
PublishAsync($"{Global.mqtt_discovery_prefix}/button/{Global.mqtt_prefix}/button{i}/config", null);
continue;
}
@ -381,8 +421,19 @@ namespace OmniLinkBridge.Modules
PublishAsync(button.ToTopic(Topic.state), "OFF");
PublishAsync(button.ToTopic(Topic.name), button.Name);
PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/button{i}/config",
JsonConvert.SerializeObject(button.ToConfig()));
if (Global.mqtt_discovery_button_type == typeof(Switch))
{
PublishAsync($"{Global.mqtt_discovery_prefix}/button/{Global.mqtt_prefix}/button{i}/config", null);
PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/button{i}/config",
JsonConvert.SerializeObject(button.ToConfigSwitch()));
}
else if (Global.mqtt_discovery_button_type == typeof(Button))
{
PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/button{i}/config", null);
PublishAsync($"{Global.mqtt_discovery_prefix}/button/{Global.mqtt_prefix}/button{i}/config",
JsonConvert.SerializeObject(button.ToConfigButton()));
}
}
}
@ -400,12 +451,103 @@ namespace OmniLinkBridge.Modules
continue;
}
PublishMessageState(message);
PublishMessageStateAsync(message);
PublishAsync(message.ToTopic(Topic.name), message.Name);
}
}
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 PublishAudioSources()
{
log.Debug("Publishing {type}", "audio sources");
for (ushort i = 1; i <= OmniLink.Controller.AudioSources.Count; i++)
{
clsAudioSource audioSource = OmniLink.Controller.AudioSources[i];
if (audioSource.DefaultProperties == true)
{
PublishAsync(audioSource.ToTopic(Topic.name), null);
continue;
}
PublishAsync(audioSource.ToTopic(Topic.name), audioSource.rawName);
if (AudioSources.ContainsKey(audioSource.rawName))
{
log.Warning("Duplicate audio source name {name}", audioSource.rawName);
continue;
}
AudioSources.Add(audioSource.rawName, i);
}
}
private void PublishAudioZones()
{
log.Debug("Publishing {type}", "audio zones");
for (ushort i = 1; i <= OmniLink.Controller.AudioZones.Count; i++)
{
clsAudioZone audioZone = OmniLink.Controller.AudioZones[i];
if (audioZone.DefaultProperties == true)
{
PublishAsync(audioZone.ToTopic(Topic.name), null);
PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/audio{i}/config", null);
PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/audio{i}mute/config", null);
PublishAsync($"{Global.mqtt_discovery_prefix}/select/{Global.mqtt_prefix}/audio{i}source/config", null);
PublishAsync($"{Global.mqtt_discovery_prefix}/number/{Global.mqtt_prefix}/audio{i}volume/config", null);
continue;
}
PublishAudioZoneStateAsync(audioZone);
PublishAsync(audioZone.ToTopic(Topic.name), audioZone.rawName);
PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/audio{i}/config",
JsonConvert.SerializeObject(audioZone.ToConfig()));
PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/audio{i}mute/config",
JsonConvert.SerializeObject(audioZone.ToConfigMute()));
PublishAsync($"{Global.mqtt_discovery_prefix}/select/{Global.mqtt_prefix}/audio{i}source/config",
JsonConvert.SerializeObject(audioZone.ToConfigSource(new List<string>(AudioSources.Keys))));
PublishAsync($"{Global.mqtt_discovery_prefix}/number/{Global.mqtt_prefix}/audio{i}volume/config",
JsonConvert.SerializeObject(audioZone.ToConfigVolume()));
}
PublishAsync($"{Global.mqtt_discovery_prefix}/button/{Global.mqtt_prefix}/audio0/config",
JsonConvert.SerializeObject(new Button(MqttDeviceRegistry)
{
unique_id = $"{Global.mqtt_prefix}audio0",
name = Global.mqtt_discovery_name_prefix + "Audio All Off",
icon = "mdi:speaker",
command_topic = $"{Global.mqtt_prefix}/audio0/{Topic.command}",
payload_press = "OFF"
}));
}
private void Omnilink_OnAreaStatus(object sender, AreaStatusEventArgs e)
{
if (!MqttClient.IsConnected)
@ -462,8 +604,17 @@ namespace OmniLinkBridge.Modules
return;
// Ignore events fired by thermostat polling
if (!e.EventTimer)
PublishThermostatState(e.Thermostat);
if (e.EventTimer)
return;
if (e.Offline)
{
PublishAsync(e.Thermostat.ToTopic(Topic.status), OFFLINE);
return;
}
PublishAsync(e.Thermostat.ToTopic(Topic.status), ONLINE);
PublishThermostatState(e.Thermostat);
}
private async void OmniLink_OnButtonStatus(object sender, ButtonStatusEventArgs e)
@ -471,7 +622,7 @@ namespace OmniLinkBridge.Modules
if (!MqttClient.IsConnected)
return;
await PublishButtonState(e.Button);
await PublishButtonStateAsync(e.Button);
}
private void OmniLink_OnMessageStatus(object sender, MessageStatusEventArgs e)
@ -479,7 +630,23 @@ namespace OmniLinkBridge.Modules
if (!MqttClient.IsConnected)
return;
PublishMessageState(e.Message);
PublishMessageStateAsync(e.Message);
}
private void OmniLink_OnLockStatus(object sender, LockStatusEventArgs e)
{
if (!MqttClient.IsConnected)
return;
PublishLockStateAsync(e.Reader);
}
private void OmniLink_OnAudioZoneStatus(object sender, AudioZoneStatusEventArgs e)
{
if (!MqttClient.IsConnected)
return;
PublishAudioZoneStateAsync(e.AudioZone);
}
private void OmniLink_OnSystemStatus(object sender, SystemStatusEventArgs e)
@ -488,13 +655,13 @@ namespace OmniLinkBridge.Modules
return;
if(e.Type == SystemEventType.Phone)
PublishAsync(SystemTroubleTopic("phone"), e.Trouble ? "trouble" : "secure");
PublishAsync(SystemTroubleTopic("phone"), e.Trouble ? TROUBLE : SECURE);
else if (e.Type == SystemEventType.AC)
PublishAsync(SystemTroubleTopic("ac"), e.Trouble ? "trouble" : "secure");
PublishAsync(SystemTroubleTopic("ac"), e.Trouble ? TROUBLE : SECURE);
else if (e.Type == SystemEventType.Button)
PublishAsync(SystemTroubleTopic("battery"), e.Trouble ? "trouble" : "secure");
PublishAsync(SystemTroubleTopic("battery"), e.Trouble ? TROUBLE : SECURE);
else if (e.Type == SystemEventType.DCM)
PublishAsync(SystemTroubleTopic("dcm"), e.Trouble ? "trouble" : "secure");
PublishAsync(SystemTroubleTopic("dcm"), e.Trouble ? TROUBLE : SECURE);
}
private void PublishAreaState(clsArea area)
@ -519,7 +686,11 @@ namespace OmniLinkBridge.Modules
{
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.scene_state), unit.ToSceneState());
@ -535,12 +706,13 @@ namespace OmniLinkBridge.Modules
PublishAsync(thermostat.ToTopic(Topic.temperature_cool_state), thermostat.CoolSetpointText());
PublishAsync(thermostat.ToTopic(Topic.humidify_state), thermostat.HumidifySetpointText());
PublishAsync(thermostat.ToTopic(Topic.dehumidify_state), thermostat.DehumidifySetpointText());
PublishAsync(thermostat.ToTopic(Topic.mode_state), thermostat.ModeText().ToLower());
PublishAsync(thermostat.ToTopic(Topic.mode_state), thermostat.ToModeState());
PublishAsync(thermostat.ToTopic(Topic.mode_basic_state), thermostat.ToModeBasicState());
PublishAsync(thermostat.ToTopic(Topic.fan_mode_state), thermostat.FanModeText().ToLower());
PublishAsync(thermostat.ToTopic(Topic.hold_state), thermostat.HoldStatusText().ToLower());
}
private async Task PublishButtonState(clsButton button)
private async Task PublishButtonStateAsync(clsButton button)
{
// Simulate a momentary press
await PublishAsync(button.ToTopic(Topic.state), "ON");
@ -548,9 +720,23 @@ namespace OmniLinkBridge.Modules
await PublishAsync(button.ToTopic(Topic.state), "OFF");
}
private void PublishMessageState(clsMessage message)
private Task PublishMessageStateAsync(clsMessage message)
{
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 void PublishAudioZoneStateAsync(clsAudioZone audioZone)
{
PublishAsync(audioZone.ToTopic(Topic.state), audioZone.ToState());
PublishAsync(audioZone.ToTopic(Topic.mute_state), audioZone.ToMuteState());
PublishAsync(audioZone.ToTopic(Topic.source_state),
OmniLink.Controller.AudioSources[audioZone.ToSourceState()].rawName);
PublishAsync(audioZone.ToTopic(Topic.volume_state), audioZone.ToVolumeState().ToString());
}
private Task PublishAsync(string topic, string payload)

View File

@ -40,6 +40,8 @@ namespace OmniLinkBridge.Modules
public event EventHandler<UnitStatusEventArgs> OnUnitStatus;
public event EventHandler<ButtonStatusEventArgs> OnButtonStatus;
public event EventHandler<MessageStatusEventArgs> OnMessageStatus;
public event EventHandler<LockStatusEventArgs> OnLockStatus;
public event EventHandler<AudioZoneStatusEventArgs> OnAudioZoneStatus;
public event EventHandler<SystemStatusEventArgs> OnSystemStatus;
private readonly AutoResetEvent trigger = new AutoResetEvent(false);
@ -51,7 +53,7 @@ namespace OmniLinkBridge.Modules
Controller.Connection.NetworkAddress = address;
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.Connection.ConnectionType = enuOmniLinkConnectionType.Network_TCP;
@ -85,6 +87,7 @@ namespace OmniLinkBridge.Modules
public bool SendCommand(enuUnitCommand Cmd, byte Par, ushort Pr2)
{
log.Verbose("Sending: {command}, Par1: {par1}, Par2: {par2}", Cmd, Par, Pr2);
return Controller.SendCommand(Cmd, Par, Pr2);
}
@ -95,16 +98,18 @@ namespace OmniLinkBridge.Modules
{
retry = DateTime.Now.AddMinutes(1);
log.Debug("Controller: {connectionStatus}", "Connect");
Controller.Connection.Connect(HandleConnectStatus, HandleUnsolicitedPackets);
}
}
private void Disconnect()
{
log.Debug("Controller Status: {connectionStatus}", "Disconnecting");
if (Controller.Connection.ConnectionState != enuOmniLinkConnectionState.Offline)
{
log.Debug("Controller: {connectionStatus}", "Disconnect");
Controller.Connection.Disconnect();
}
}
private void HandleConnectStatus(enuOmniLinkCommStatus CS)
@ -135,61 +140,8 @@ namespace OmniLinkBridge.Modules
log.Warning("Controller Status: {connectionStatus}", status);
break;
case enuOmniLinkCommStatus.NoReply:
case enuOmniLinkCommStatus.UnrecognizedReply:
case enuOmniLinkCommStatus.UnsupportedProtocol:
case enuOmniLinkCommStatus.ClientSessionTerminated:
case enuOmniLinkCommStatus.ControllerSessionTerminated:
case enuOmniLinkCommStatus.CannotStartNewSession:
case enuOmniLinkCommStatus.LoginFailed:
case enuOmniLinkCommStatus.UnableToOpenSocket:
case enuOmniLinkCommStatus.UnableToConnect:
case enuOmniLinkCommStatus.SocketClosed:
case enuOmniLinkCommStatus.UnexpectedError:
case enuOmniLinkCommStatus.UnableToCreateSocket:
case enuOmniLinkCommStatus.PermissionDenied:
case enuOmniLinkCommStatus.BadAddress:
case enuOmniLinkCommStatus.InvalidArgument:
case enuOmniLinkCommStatus.TooManyOpenFiles:
case enuOmniLinkCommStatus.ResourceTemporarilyUnavailable:
case enuOmniLinkCommStatus.SocketOperationOnNonSocket:
case enuOmniLinkCommStatus.DestinationAddressRequired:
case enuOmniLinkCommStatus.MessgeTooLong:
case enuOmniLinkCommStatus.WrongProtocolType:
case enuOmniLinkCommStatus.BadProtocolOption:
case enuOmniLinkCommStatus.ProtocolNotSupported:
case enuOmniLinkCommStatus.SocketTypeNotSupported:
case enuOmniLinkCommStatus.OperationNotSupported:
case enuOmniLinkCommStatus.ProtocolFamilyNotSupported:
case enuOmniLinkCommStatus.AddressFamilyNotSupported:
case enuOmniLinkCommStatus.AddressInUse:
case enuOmniLinkCommStatus.AddressNotAvailable:
case enuOmniLinkCommStatus.NetworkIsDown:
case enuOmniLinkCommStatus.NetworkIsUnreachable:
case enuOmniLinkCommStatus.NetworkReset:
case enuOmniLinkCommStatus.ConnectionAborted:
case enuOmniLinkCommStatus.ConnectionResetByPeer:
case enuOmniLinkCommStatus.NoBufferSpaceAvailable:
case enuOmniLinkCommStatus.NotConnected:
case enuOmniLinkCommStatus.CannotSendAfterShutdown:
case enuOmniLinkCommStatus.ConnectionTimedOut:
case enuOmniLinkCommStatus.ConnectionRefused:
case enuOmniLinkCommStatus.HostIsDown:
case enuOmniLinkCommStatus.HostUnreachable:
case enuOmniLinkCommStatus.TooManyProcesses:
case enuOmniLinkCommStatus.NetworkSubsystemIsUnavailable:
case enuOmniLinkCommStatus.UnsupportedVersion:
case enuOmniLinkCommStatus.NotInitialized:
case enuOmniLinkCommStatus.ShutdownInProgress:
case enuOmniLinkCommStatus.ClassTypeNotFound:
case enuOmniLinkCommStatus.HostNotFound:
case enuOmniLinkCommStatus.HostNotFoundTryAgain:
case enuOmniLinkCommStatus.NonRecoverableError:
case enuOmniLinkCommStatus.NoDataOfRequestedType:
log.Error("Controller Status: {connectionStatus}", status);
break;
default:
log.Error("Controller Status: {connectionStatus}", status);
break;
}
}
@ -203,7 +155,7 @@ namespace OmniLinkBridge.Modules
}
}
private void HandleIdentifyController(clsOmniLinkMessageQueueItem M, byte[] B, bool Timeout)
private async void HandleIdentifyController(clsOmniLinkMessageQueueItem M, byte[] B, bool Timeout)
{
if (Timeout)
return;
@ -229,8 +181,7 @@ namespace OmniLinkBridge.Modules
log.Information("Controller is {ControllerModel} firmware {ControllerVersion}",
Controller.GetModelText(), Controller.GetVersionText());
_ = Connected();
await ConnectedAsync();
return;
}
@ -239,7 +190,7 @@ namespace OmniLinkBridge.Modules
}
}
private async Task Connected()
private async Task ConnectedAsync()
{
retry = DateTime.MinValue;
@ -250,6 +201,8 @@ namespace OmniLinkBridge.Modules
tstat_timer.Start();
OnConnect?.Invoke(this, new EventArgs());
Program.ShowSendLogsWarning();
}
#endregion
@ -266,6 +219,9 @@ namespace OmniLinkBridge.Modules
await GetNamed(enuObjectType.Unit);
await GetNamed(enuObjectType.Message);
await GetNamed(enuObjectType.Button);
await GetNamed(enuObjectType.AccessControlReader);
await GetNamed(enuObjectType.AudioSource);
await GetNamed(enuObjectType.AudioZone);
}
private async Task GetSystemFormats()
@ -277,7 +233,8 @@ namespace OmniLinkBridge.Modules
await Task.Run(() =>
{
nameWait.WaitOne(new TimeSpan(0, 0, 10));
if(!nameWait.WaitOne(new TimeSpan(0, 0, 10)))
log.Error("Timeout occurred waiting system formats");
});
}
@ -290,7 +247,8 @@ namespace OmniLinkBridge.Modules
await Task.Run(() =>
{
nameWait.WaitOne(new TimeSpan(0, 0, 10));
if(!nameWait.WaitOne(new TimeSpan(0, 0, 10)))
log.Error("Timeout occurred waiting for system troubles");
});
}
@ -302,7 +260,8 @@ namespace OmniLinkBridge.Modules
await Task.Run(() =>
{
nameWait.WaitOne(new TimeSpan(0, 0, 10));
if (!nameWait.WaitOne(new TimeSpan(0, 0, 30)))
log.Error("Timeout occurred waiting for named units {unitType}", type.ToString());
});
}
@ -368,7 +327,8 @@ namespace OmniLinkBridge.Modules
Controller.Zones.CopyProperties(MSG);
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;
case enuObjectType.Thermostat:
@ -379,7 +339,8 @@ namespace OmniLinkBridge.Modules
else
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}",
Controller.Thermostats[MSG.ObjectNumber].Name);
break;
@ -392,6 +353,17 @@ namespace OmniLinkBridge.Modules
case enuObjectType.Button:
Controller.Buttons.CopyProperties(MSG);
break;
case enuObjectType.AccessControlReader:
Controller.AccessControlReaders.CopyProperties(MSG);
break;
case enuObjectType.AudioSource:
Controller.AudioSources.CopyProperties(MSG);
Controller.AudioSources[MSG.ObjectNumber].rawName = MSG.ObjectName;
break;
case enuObjectType.AudioZone:
Controller.AudioZones.CopyProperties(MSG);
Controller.AudioZones[MSG.ObjectNumber].rawName = MSG.ObjectName;
break;
default:
break;
}
@ -472,6 +444,8 @@ namespace OmniLinkBridge.Modules
case enuOmniLink2MessageType.CmdExtSecurity:
break;
case enuOmniLink2MessageType.AudioSourceStatus:
// Ignore audio source metadata status updates
handled = true;
break;
case enuOmniLink2MessageType.SystemEvents:
HandleUnsolicitedSystemEvent(B);
@ -688,19 +662,13 @@ namespace OmniLinkBridge.Modules
{
Controller.Thermostats[MSG.ObjectNumber(i)].CopyExtendedStatus(MSG, i);
// Don't fire event when invalid temperature of 0 is sometimes received
if (Controller.Thermostats[MSG.ObjectNumber(i)].Temp > 0)
OnThermostatStatus?.Invoke(this, new ThermostatStatusEventArgs()
{
OnThermostatStatus?.Invoke(this, new ThermostatStatusEventArgs()
{
ID = MSG.ObjectNumber(i),
Thermostat = Controller.Thermostats[MSG.ObjectNumber(i)],
EventTimer = false
});
}
else if (Global.verbose_thermostat_timer)
log.Debug("Ignoring unsolicited unknown temp for Thermostat {thermostatName}",
Controller.Thermostats[MSG.ObjectNumber(i)].Name);
ID = MSG.ObjectNumber(i),
Thermostat = Controller.Thermostats[MSG.ObjectNumber(i)],
Offline = Controller.Thermostats[MSG.ObjectNumber(i)].Temp == 0,
EventTimer = false
});
if (!tstats.ContainsKey(MSG.ObjectNumber(i)))
tstats.Add(MSG.ObjectNumber(i), DateTime.Now);
@ -735,6 +703,28 @@ namespace OmniLinkBridge.Modules
});
}
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;
case enuObjectType.AudioZone:
for (byte i = 0; i < MSG.AudioZoneStatusCount(); i++)
{
Controller.AudioZones[MSG.ObjectNumber(i)].CopyExtendedStatus(MSG, i);
OnAudioZoneStatus?.Invoke(this, new AudioZoneStatusEventArgs()
{
ID = MSG.ObjectNumber(i),
AudioZone = Controller.AudioZones[MSG.ObjectNumber(i)]
});
}
break;
default:
if (Global.verbose_unhandled)
{
@ -768,9 +758,12 @@ namespace OmniLinkBridge.Modules
foreach (KeyValuePair<ushort, DateTime> tstat in tstats)
{
// Poll every 4 minutes if no prior update
if (RoundToMinute(tstat.Value).AddMinutes(4) <= RoundToMinute(DateTime.Now))
if (RoundToMinute(tstat.Value).AddMinutes(4) <= RoundToMinute(DateTime.Now) &&
(Controller.Connection.ConnectionState == enuOmniLinkConnectionState.Online ||
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)
log.Debug("Polling status for Thermostat {thermostatName}",
@ -782,19 +775,13 @@ namespace OmniLinkBridge.Modules
(Controller.Connection.ConnectionState == enuOmniLinkConnectionState.Online ||
Controller.Connection.ConnectionState == enuOmniLinkConnectionState.OnlineSecure))
{
// Don't fire event when invalid temperature of 0 is sometimes received
if (Controller.Thermostats[tstat.Key].Temp > 0)
OnThermostatStatus?.Invoke(this, new ThermostatStatusEventArgs()
{
OnThermostatStatus?.Invoke(this, new ThermostatStatusEventArgs()
{
ID = tstat.Key,
Thermostat = Controller.Thermostats[tstat.Key],
EventTimer = true
});
}
else if (Global.verbose_thermostat_timer)
log.Warning("Ignoring unknown temp for Thermostat {thermostatName}",
Controller.Thermostats[tstat.Key].Name);
ID = tstat.Key,
Thermostat = Controller.Thermostats[tstat.Key],
Offline = Controller.Thermostats[tstat.Key].Temp == 0,
EventTimer = true
});
}
else if (Global.verbose_thermostat_timer)
log.Warning("Not logging out of date status for Thermostat {thermostatName}",

View File

@ -74,14 +74,15 @@ namespace OmniLinkBridge.Modules
// Extract the 2 digit prefix to use when parsing the time
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
{
log.Warning("Controller time could not be parsed");
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);
return;
@ -92,10 +93,11 @@ namespace OmniLinkBridge.Modules
if (adj > Global.time_drift)
{
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;
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);
}
}

View File

@ -33,6 +33,8 @@ namespace OmniLinkBridge
public void Startup()
{
log.Warning("WebAPI is deprecated");
WebNotification.RestoreSubscriptions();
Uri uri = new Uri("http://0.0.0.0:" + Global.webapi_port + "/");

View File

@ -26,23 +26,21 @@ namespace OmniLinkBridge.Notifications
};
mail.To.Add(address);
using (SmtpClient smtp = new SmtpClient(Global.mail_server, Global.mail_port))
using SmtpClient smtp = new SmtpClient(Global.mail_server, Global.mail_port);
smtp.EnableSsl = Global.mail_tls;
if (!string.IsNullOrEmpty(Global.mail_username))
{
smtp.EnableSsl = Global.mail_tls;
if (!string.IsNullOrEmpty(Global.mail_username))
{
smtp.UseDefaultCredentials = false;
smtp.Credentials = new NetworkCredential(Global.mail_username, Global.mail_password);
}
smtp.UseDefaultCredentials = false;
smtp.Credentials = new NetworkCredential(Global.mail_username, Global.mail_password);
}
try
{
smtp.Send(mail);
}
catch (Exception ex)
{
log.Error(ex, "An error occurred sending email notification");
}
try
{
smtp.Send(mail);
}
catch (Exception ex)
{
log.Error(ex, "An error occurred sending email notification");
}
}
}

View File

@ -25,12 +25,10 @@ namespace OmniLinkBridge.Notifications
"description=" + description
};
using (WebClient client = new WebClient())
{
client.Headers[HttpRequestHeader.ContentType] = "application/x-www-form-urlencoded";
client.UploadStringAsync(URI, string.Join("&", parameters.ToArray()));
client.UploadStringCompleted += Client_UploadStringCompleted;
}
using WebClient client = new WebClient();
client.Headers[HttpRequestHeader.ContentType] = "application/x-www-form-urlencoded";
client.UploadStringAsync(URI, string.Join("&", parameters.ToArray()));
client.UploadStringCompleted += Client_UploadStringCompleted;
}
}

View File

@ -24,11 +24,9 @@ namespace OmniLinkBridge.Notifications
{ "message", description }
};
using (WebClient client = new WebClient())
{
client.UploadValues(URI, parameters);
client.UploadStringCompleted += Client_UploadStringCompleted;
}
using WebClient client = new WebClient();
client.UploadValues(URI, parameters);
client.UploadStringCompleted += Client_UploadStringCompleted;
}
}

View File

@ -0,0 +1,11 @@
using HAI_Shared;
using System;
namespace OmniLinkBridge.OmniLink
{
public class AudioZoneStatusEventArgs : EventArgs
{
public ushort ID { get; set; }
public clsAudioZone AudioZone { get; set; }
}
}

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

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OmniLinkBridge.OmniLink
namespace OmniLinkBridge.OmniLink
{
public enum SystemEventType
{

View File

@ -1,5 +1,4 @@
using HAI_Shared;
using System;
using System;
namespace OmniLinkBridge.OmniLink
{

View File

@ -8,6 +8,11 @@ namespace OmniLinkBridge.OmniLink
public ushort ID { get; set; }
public clsThermostat Thermostat { get; set; }
/// <summary>
/// Set to true when thermostat is offline, indicated by a temperature of 0
/// </summary>
public bool Offline { get; set; }
/// <summary>
/// Set to true when fired by thermostat polling
/// </summary>

View File

@ -10,7 +10,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>OmniLinkBridge</RootNamespace>
<AssemblyName>OmniLinkBridge</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<TargetFrameworkProfile />
</PropertyGroup>
@ -80,27 +80,39 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="ControllerEnricher.cs" />
<Compile Include="CoreServer.cs" />
<Compile Include="Modules\TimeSyncModule.cs" />
<Compile Include="MQTT\Alarm.cs" />
<Compile Include="MQTT\AlarmCommands.cs" />
<Compile Include="MQTT\AreaCommands.cs" />
<Compile Include="MQTT\HomeAssistant\Button.cs" />
<Compile Include="MQTT\HomeAssistant\Alarm.cs" />
<Compile Include="MQTT\HomeAssistant\Lock.cs" />
<Compile Include="MQTT\HomeAssistant\Select.cs" />
<Compile Include="MQTT\OverrideArea.cs" />
<Compile Include="MQTT\OverrideUnit.cs" />
<Compile Include="MQTT\Parser\AlarmCommands.cs" />
<Compile Include="MQTT\AreaCommandCode.cs" />
<Compile Include="MQTT\Parser\AreaCommands.cs" />
<Compile Include="MQTT\AreaState.cs" />
<Compile Include="MQTT\BinarySensor.cs" />
<Compile Include="MQTT\CommandTypes.cs" />
<Compile Include="MQTT\Device.cs" />
<Compile Include="MQTT\Climate.cs" />
<Compile Include="MQTT\DeviceRegistry.cs" />
<Compile Include="MQTT\MessageCommands.cs" />
<Compile Include="MQTT\Availability.cs" />
<Compile Include="MQTT\HomeAssistant\BinarySensor.cs" />
<Compile Include="MQTT\Parser\CommandTypes.cs" />
<Compile Include="MQTT\HomeAssistant\Device.cs" />
<Compile Include="MQTT\HomeAssistant\Climate.cs" />
<Compile Include="MQTT\HomeAssistant\DeviceRegistry.cs" />
<Compile Include="MQTT\Extensions.cs" />
<Compile Include="MQTT\Parser\LockCommands.cs" />
<Compile Include="MQTT\Parser\MessageCommands.cs" />
<Compile Include="MQTT\MessageProcessor.cs" />
<Compile Include="MQTT\HomeAssistant\Number.cs" />
<Compile Include="MQTT\OverrideZone.cs" />
<Compile Include="MQTT\Switch.cs" />
<Compile Include="MQTT\Light.cs" />
<Compile Include="MQTT\HomeAssistant\Switch.cs" />
<Compile Include="MQTT\HomeAssistant\Light.cs" />
<Compile Include="MQTT\MappingExtensions.cs" />
<Compile Include="MQTT\Sensor.cs" />
<Compile Include="MQTT\Topic.cs" />
<Compile Include="MQTT\UnitCommands.cs" />
<Compile Include="MQTT\ZoneCommands.cs" />
<Compile Include="MQTT\HomeAssistant\Sensor.cs" />
<Compile Include="MQTT\Parser\Topic.cs" />
<Compile Include="MQTT\Parser\UnitCommands.cs" />
<Compile Include="MQTT\Parser\ZoneCommands.cs" />
<Compile Include="MQTT\UnitType.cs" />
<Compile Include="Notifications\EmailNotification.cs" />
<Compile Include="Notifications\INotification.cs" />
<Compile Include="Notifications\Notification.cs" />
@ -108,7 +120,9 @@
<Compile Include="Notifications\PushoverNotification.cs" />
<Compile Include="OmniLink\ButtonStatusEventArgs.cs" />
<Compile Include="OmniLink\IOmniLinkII.cs" />
<Compile Include="OmniLink\AudioZoneStatusEventArgs.cs" />
<Compile Include="OmniLink\SystemEventType.cs" />
<Compile Include="OmniLink\LockStatusEventArgs.cs" />
<Compile Include="OmniLink\UnitStatusEventArgs.cs" />
<Compile Include="OmniLink\ThermostatStatusEventArgs.cs" />
<Compile Include="OmniLink\MessageStatusEventArgs.cs" />
@ -173,29 +187,32 @@
<Version>4.5.0</Version>
</PackageReference>
<PackageReference Include="MQTTnet.Extensions.ManagedClient">
<Version>3.0.13</Version>
<Version>3.1.2</Version>
</PackageReference>
<PackageReference Include="Newtonsoft.Json">
<Version>12.0.3</Version>
<Version>13.0.3</Version>
</PackageReference>
<PackageReference Include="Serilog">
<Version>2.10.0</Version>
</PackageReference>
<PackageReference Include="Serilog.Formatting.Compact">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Serilog.Sinks.Async">
<Version>1.4.0</Version>
</PackageReference>
<PackageReference Include="Serilog.Sinks.Console">
<Version>3.1.1</Version>
</PackageReference>
<PackageReference Include="Serilog.Formatting.Compact">
<Version>2.0.0</Version>
</PackageReference>
<PackageReference Include="Serilog.Sinks.Async">
<Version>1.5.0</Version>
</PackageReference>
<PackageReference Include="Serilog.Sinks.Console">
<Version>5.0.1</Version>
</PackageReference>
<PackageReference Include="Serilog.Sinks.File">
<Version>4.1.0</Version>
<Version>5.0.0</Version>
</PackageReference>
<PackageReference Include="Serilog.Sinks.Http">
<Version>7.2.0</Version>
</PackageReference>
<PackageReference Include="System.ValueTuple">
<Version>4.5.0</Version>
</PackageReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.

View File

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

View File

@ -22,6 +22,8 @@ verbose_thermostat_timer = yes
verbose_thermostat = yes
verbose_unit = yes
verbose_message = yes
verbose_lock = yes
verbose_audio = yes
# mySQL Logging (yes/no)
mysql_logging = no
@ -49,13 +51,44 @@ mqtt_prefix = omnilink
mqtt_discovery_prefix = homeassistant
# Prefix for Home Assistant entity names
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_units =
# device_class must be battery, door, garage_door, gas, moisture, motion, problem, smoke, or window
# Override the area Home Assistant alarm control panel
# Prompt for user code
# code_arm: true or false, defaults to false
# code_disarm: true or false, defaults to false
# Show these modes
# arm_home: true or false, defaults to true
# arm_away: true or false, defaults to true
# arm_night: true or false, defaults to true
# arm_vacation: true or false, defaults to true
#mqtt_discovery_override_area = id=1;code_disarm=true;arm_vacation=false
# 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=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
# Publish buttons as this Home Assistant device type
# must be button (recommended) or switch (default, previous behavior)
mqtt_discovery_button_type = switch
# Handle mute locally by setting volume to 0 and restoring to previous value
mqtt_audio_local_mute = no
# Change audio volume scaling for Home Assistant media player
# yes 0.00-1.00, no 0-100 (default, previous behavior)
mqtt_audio_volume_media_player = no
# Notifications (yes/no)
# Always sent for area alarms and critical system events
# Optionally enable for area status changes and console messages

View File

@ -4,7 +4,9 @@ using Serilog.Events;
using Serilog.Filters;
using Serilog.Formatting.Compact;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.ServiceProcess;
@ -12,11 +14,11 @@ using System.Threading.Tasks;
namespace OmniLinkBridge
{
class Program
internal class Program
{
static CoreServer server;
private static CoreServer server;
static int Main(string[] args)
private static int Main(string[] args)
{
bool interactive = false;
@ -55,6 +57,10 @@ namespace OmniLinkBridge
case "-ll":
Enum.TryParse(args[++i], out log_level);
break;
case "-ld":
Global.DebugSettings = true;
Global.SendLogs = true;
break;
case "-s":
Global.webapi_subscriptions_file = args[++i];
break;
@ -64,6 +70,12 @@ namespace OmniLinkBridge
}
}
if (string.Compare(Environment.GetEnvironmentVariable("SEND_LOGS"), "1") == 0)
{
Global.DebugSettings = true;
Global.SendLogs = true;
}
config_file = GetFullPath(config_file);
Global.webapi_subscriptions_file = GetFullPath(Global.webapi_subscriptions_file ?? "WebSubscriptions.json");
@ -76,8 +88,8 @@ namespace OmniLinkBridge
var log_config = new LoggerConfiguration()
.MinimumLevel.Verbose()
.Enrich.WithProperty("Application", "OmniLinkBridge")
.Enrich.WithProperty("Session", Guid.NewGuid())
.Enrich.WithProperty("User", (Environment.UserName + Environment.MachineName).GetHashCode())
.Enrich.WithProperty("Session", Global.SessionID)
.Enrich.With<ControllerEnricher>()
.Enrich.FromLogContext();
if (log_file != null)
@ -92,7 +104,10 @@ namespace OmniLinkBridge
rollingInterval: RollingInterval.Day, retainedFileCountLimit: 15));
}
if (UseTelemetry())
if (Global.SendLogs)
log_config = log_config.WriteTo.Logger(lc => lc
.WriteTo.Http("https://telemetry.excalibur-partners.com"));
else if (UseTelemetry())
log_config = log_config.WriteTo.Logger(lc => lc
.Filter.ByIncludingOnly(Matching.WithProperty("Telemetry"))
.WriteTo.Http("https://telemetry.excalibur-partners.com"));
@ -155,7 +170,7 @@ namespace OmniLinkBridge
return 0;
}
static string GetFullPath(string file)
private static string GetFullPath(string file)
{
if (Path.IsPathRooted(file))
return file;
@ -169,21 +184,38 @@ namespace OmniLinkBridge
args.Cancel = true;
}
static bool IsRunningOnMono()
private static bool IsRunningOnMono()
{
return Type.GetType("Mono.Runtime") != null;
}
static bool UseTelemetry()
public static string GetEnvironment()
{
if (Environment.GetEnvironmentVariable("HASSIO_TOKEN") != null)
return "Home Assistant";
else if (IsRunningOnMono())
return Process.GetProcesses().Any(w => w.Id == 2) ? "Mono" : "Docker";
else
return "Native";
}
private static bool UseTelemetry()
{
return string.Compare(Environment.GetEnvironmentVariable("TELEMETRY_OPTOUT"), "1") != 0;
}
static void ShowHelp()
public static void ShowSendLogsWarning()
{
if (Global.SendLogs)
Log.Warning("SENDING LOGS TO DEVELOPER Controller: {ControllerID}, Session: {Session}",
Global.controller_id, Global.SessionID);
}
private static void ShowHelp()
{
Console.WriteLine(
AppDomain.CurrentDomain.FriendlyName + " [-c config_file] [-e] [-d] [-j] [-s subscriptions_file]\n" +
"\t[-lf log_file|disable] [-lj [-ll verbose|debug|information|warning|error] [-i]\n" +
"\t[-lf log_file|disable] [-lj [-ll verbose|debug|information|warning|error] [-ld] [-i]\n" +
"\t-c Specifies the configuration file. Default is OmniLinkBridge.ini\n" +
"\t-e Check environment variables for configuration settings\n" +
"\t-d Show debug ouput for configuration loading\n" +
@ -191,8 +223,14 @@ namespace OmniLinkBridge
"\t-lf Specifies the rolling log file. Retention is 15 days. Default is log.txt.\n" +
"\t-lj Write logs as CLEF (compact log event format) JSON.\n" +
"\t-ll Minimum level at which events will be logged. Default is information.\n" +
"\t-ld Send logs to developer. ONLY USE WHEN ASKED.\n" +
"\t Also enabled by setting a SEND_LOGS environment variable to 1.\n" +
"\t-i Run in interactive mode");
Console.WriteLine(
"\nVersion: " + Assembly.GetExecutingAssembly().GetName().Version +
"\nEnvironment: " + GetEnvironment());
Console.WriteLine(
"\nOmniLink Bridge collects anonymous telemetry data to help improve the software.\n" +
"You can opt of telemetry by setting a TELEMETRY_OPTOUT environment variable to 1.");

View File

@ -10,7 +10,7 @@ using System.Runtime.InteropServices;
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Excalibur Partners, LLC")]
[assembly: AssemblyProduct("OmniLinkBridge")]
[assembly: AssemblyCopyright("Copyright © Excalibur Partners, LLC 2020")]
[assembly: AssemblyCopyright("Copyright © Excalibur Partners, LLC 2024")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
@ -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.10.0")]
[assembly: AssemblyFileVersion("1.1.10.0")]
[assembly: AssemblyVersion("1.1.19.0")]
[assembly: AssemblyFileVersion("1.1.19.0")]

View File

@ -1,3 +1,4 @@
using OmniLinkBridge.MQTT.HomeAssistant;
using Serilog;
using System;
using System.Collections.Concurrent;
@ -7,7 +8,7 @@ using System.IO;
using System.Linq;
using System.Net.Mail;
using System.Reflection;
using System.Threading;
using ha = OmniLinkBridge.MQTT.HomeAssistant;
namespace OmniLinkBridge
{
@ -30,9 +31,10 @@ namespace OmniLinkBridge
// HAI / Leviton Omni Controller
Global.controller_address = settings.ValidateHasValue("controller_address");
Global.controller_port = settings.ValidatePort("controller_port");
Global.controller_key1 = settings.ValidateHasValue("controller_key1");
Global.controller_key2 = settings.ValidateHasValue("controller_key2");
Global.controller_key1 = settings.ValidateEncryptionKey("controller_key1");
Global.controller_key2 = settings.ValidateEncryptionKey("controller_key2");
Global.controller_name = settings.CheckEnv("controller_name") ?? "OmniLinkBridge";
Global.controller_id = (Global.controller_address + Global.controller_key1 + Global.controller_key2).ComputeGuid();
// Controller Time Sync
Global.time_sync = settings.ValidateBool("time_sync");
@ -52,10 +54,12 @@ namespace OmniLinkBridge
Global.verbose_thermostat = settings.ValidateBool("verbose_thermostat");
Global.verbose_unit = settings.ValidateBool("verbose_unit");
Global.verbose_message = settings.ValidateBool("verbose_message");
Global.verbose_lock = settings.ValidateBool("verbose_lock");
Global.verbose_audio = settings.ValidateBool("verbose_audio");
// mySQL Logging
Global.mysql_logging = settings.ValidateBool("mysql_logging");
Global.mysql_connection = settings.CheckEnv("mysql_connection");
Global.mysql_connection = settings.CheckEnv("mysql_connection", true);
// Web Service
Global.webapi_enabled = settings.ValidateBool("webapi_enabled");
@ -73,8 +77,8 @@ namespace OmniLinkBridge
{
Global.mqtt_server = settings.CheckEnv("mqtt_server");
Global.mqtt_port = settings.ValidatePort("mqtt_port");
Global.mqtt_username = settings.CheckEnv("mqtt_username");
Global.mqtt_password = settings.CheckEnv("mqtt_password");
Global.mqtt_username = settings.CheckEnv("mqtt_username", true);
Global.mqtt_password = settings.CheckEnv("mqtt_password", true);
Global.mqtt_prefix = settings.CheckEnv("mqtt_prefix") ?? "omnilink";
Global.mqtt_discovery_prefix = settings.CheckEnv("mqtt_discovery_prefix") ?? "homeassistant";
Global.mqtt_discovery_name_prefix = settings.CheckEnv("mqtt_discovery_name_prefix") ?? string.Empty;
@ -84,7 +88,12 @@ 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_override_area = settings.LoadOverrideArea<MQTT.OverrideArea>("mqtt_discovery_override_area");
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");
Global.mqtt_discovery_button_type = settings.ValidateType("mqtt_discovery_button_type", typeof(Switch), typeof(Button));
Global.mqtt_audio_local_mute = settings.ValidateBool("mqtt_audio_local_mute");
Global.mqtt_audio_volume_media_player = settings.ValidateBool("mqtt_audio_volume_media_player");
}
// Notifications
@ -98,31 +107,113 @@ namespace OmniLinkBridge
{
Global.mail_tls = settings.ValidateBool("mail_tls");
Global.mail_port = settings.ValidatePort("mail_port");
Global.mail_username = settings.CheckEnv("mail_username");
Global.mail_password = settings.CheckEnv("mail_password");
Global.mail_username = settings.CheckEnv("mail_username", true);
Global.mail_password = settings.CheckEnv("mail_password", true);
Global.mail_from = settings.ValidateMailFrom("mail_from");
Global.mail_to = settings.ValidateMailTo("mail_to");
}
// Prowl Notifications
Global.prowl_key = settings.ValidateMultipleStrings("prowl_key");
Global.prowl_key = settings.ValidateMultipleStrings("prowl_key", true);
// Pushover Notifications
Global.pushover_token = settings.CheckEnv("pushover_token");
Global.pushover_user = settings.ValidateMultipleStrings("pushover_user");
Global.pushover_token = settings.CheckEnv("pushover_token", true);
Global.pushover_user = settings.ValidateMultipleStrings("pushover_user", true);
}
private static string CheckEnv(this NameValueCollection settings, string name)
private static string CheckEnv(this NameValueCollection settings, string name, bool sensitive = false)
{
string env = Global.UseEnvironment ? Environment.GetEnvironmentVariable(name.ToUpper()) : null;
string value = !string.IsNullOrEmpty(env) ? env : settings[name];
if (Global.DebugSettings)
log.Debug((!string.IsNullOrEmpty(env) ? "ENV" : "CONF").PadRight(5) + $"{name}: {value}");
log.Debug("{ConfigType} {ConfigName}: {ConfigValue}",
(!string.IsNullOrEmpty(env) ? "ENV" : "CONF").PadRight(4), name,
sensitive && value != null ? value.Truncate(3) + "***MASKED***" : value);
return value;
}
private static ConcurrentDictionary<int, T> LoadOverrideArea<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_area = new T();
if (override_area is MQTT.OverrideArea mqtt_area)
{
foreach (string attribute in attributes.Keys)
{
switch(attribute)
{
case "id":
continue;
case "code_arm":
if (!bool.TryParse(attributes["code_arm"], out bool code_arm))
throw new Exception("Invalid code_arm attribute");
mqtt_area.code_arm = code_arm;
break;
case "code_disarm":
if (!bool.TryParse(attributes["code_disarm"], out bool code_disarm))
throw new Exception("Invalid code_disarm attribute");
mqtt_area.code_disarm = code_disarm;
break;
case "arm_home":
if (!bool.TryParse(attributes["arm_home"], out bool arm_home))
throw new Exception("Invalid arm_home attribute");
mqtt_area.arm_home = arm_home;
break;
case "arm_away":
if (!bool.TryParse(attributes["arm_away"], out bool arm_away))
throw new Exception("Invalid arm_away attribute");
mqtt_area.arm_away = arm_away;
break;
case "arm_night":
if (!bool.TryParse(attributes["arm_night"], out bool arm_night))
throw new Exception("Invalid arm_night attribute");
mqtt_area.arm_night = arm_night;
break;
case "arm_vacation":
if (!bool.TryParse(attributes["arm_vacation"], out bool arm_vacation))
throw new Exception("Invalid arm_vacation attribute");
mqtt_area.arm_vacation = arm_vacation;
break;
default:
throw new Exception($"Unknown attribute {attribute}" );
}
}
}
ret.TryAdd(attrib_id, override_area);
}
return ret;
}
catch (Exception ex)
{
log.Error(ex, "Invalid override area specified for {section}", section);
throw;
}
}
private static ConcurrentDictionary<int, T> LoadOverrideZone<T>(this NameValueCollection settings, string section) where T : new()
{
try
@ -156,7 +247,7 @@ namespace OmniLinkBridge
}
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");
mqtt_zone.device_class = attrib_device_class;
@ -174,6 +265,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)
{
string value = settings.CheckEnv(section);
@ -187,6 +322,19 @@ namespace OmniLinkBridge
return value;
}
private static string ValidateEncryptionKey(this NameValueCollection settings, string section)
{
string value = settings.CheckEnv(section, true).Replace("-","");
if (string.IsNullOrEmpty(value) || value.Length != 16)
{
log.Error("Invalid encryption key specified for {section}", section);
throw new Exception();
}
return value;
}
private static int ValidateInt(this NameValueCollection settings, string section)
{
try
@ -273,14 +421,14 @@ namespace OmniLinkBridge
}
}
private static string[] ValidateMultipleStrings(this NameValueCollection settings, string section)
private static string[] ValidateMultipleStrings(this NameValueCollection settings, string section, bool sensitive = false)
{
try
{
if (settings.CheckEnv(section) == null)
if (settings.CheckEnv(section, true) == null)
return new string[] { };
return settings.CheckEnv(section).Split(',');
return settings.CheckEnv(section, sensitive).Split(',');
}
catch
{
@ -306,6 +454,21 @@ namespace OmniLinkBridge
}
}
private static Type ValidateType(this NameValueCollection settings, string section, params Type[] types)
{
string value = settings.CheckEnv(section);
if (value == null)
return types[0];
foreach (Type type in types)
if (string.Compare(value, type.Name, true) == 0)
return type;
log.Error("Invalid type specified for {section}", section);
throw new Exception();
}
private static NameValueCollection LoadCollection(string[] lines)
{
NameValueCollection settings = new NameValueCollection();

View File

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OmniLinkBridge.WebAPI
namespace OmniLinkBridge.WebAPI
{
public enum DeviceType
{

View File

@ -1,9 +1,4 @@
using HAI_Shared;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OmniLinkBridge.WebAPI
{
@ -13,14 +8,15 @@ namespace OmniLinkBridge.WebAPI
public static AreaContract ToContract(this clsArea area)
{
AreaContract ret = new AreaContract();
ret.id = (ushort)area.Number;
ret.name = area.Name;
ret.burglary = area.AreaBurglaryAlarmText;
ret.co = area.AreaGasAlarmText;
ret.fire = area.AreaFireAlarmText;
ret.water = area.AreaWaterAlarmText;
AreaContract ret = new AreaContract
{
id = (ushort)area.Number,
name = area.Name,
burglary = area.AreaBurglaryAlarmText,
co = area.AreaGasAlarmText,
fire = area.AreaFireAlarmText,
water = area.AreaWaterAlarmText
};
if (area.ExitTimer > 0)
{
@ -37,12 +33,13 @@ namespace OmniLinkBridge.WebAPI
public static ZoneContract ToContract(this clsZone zone)
{
ZoneContract ret = new ZoneContract();
ret.id = (ushort)zone.Number;
ret.zonetype = zone.ZoneType;
ret.name = zone.Name;
ret.status = zone.StatusText();
ZoneContract ret = new ZoneContract
{
id = (ushort)zone.Number,
zonetype = zone.ZoneType,
name = zone.Name,
status = zone.StatusText()
};
if (zone.IsTemperatureZone())
ret.temp = zone.TempText();
@ -54,10 +51,11 @@ namespace OmniLinkBridge.WebAPI
public static UnitContract ToContract(this clsUnit unit)
{
UnitContract ret = new UnitContract();
ret.id = (ushort)unit.Number;
ret.name = unit.Name;
UnitContract ret = new UnitContract
{
id = (ushort)unit.Number,
name = unit.Name
};
if (unit.Status > 100)
ret.level = (ushort)(unit.Status - 100);
@ -71,17 +69,16 @@ namespace OmniLinkBridge.WebAPI
public static ThermostatContract ToContract(this clsThermostat unit)
{
ThermostatContract ret = new ThermostatContract();
ThermostatContract ret = new ThermostatContract
{
id = (ushort)unit.Number,
name = unit.Name
};
ret.id = (ushort)unit.Number;
ret.name = unit.Name;
ushort temp, heat, cool, humidity;
ushort.TryParse(unit.TempText(), out temp);
ushort.TryParse(unit.HeatSetpointText(), out heat);
ushort.TryParse(unit.CoolSetpointText(), out cool);
ushort.TryParse(unit.HumidityText(), out humidity);
ushort.TryParse(unit.TempText(), out ushort temp);
ushort.TryParse(unit.HeatSetpointText(), out ushort heat);
ushort.TryParse(unit.CoolSetpointText(), out ushort cool);
ushort.TryParse(unit.HumidityText(), out ushort humidity);
ret.temp = temp;
ret.humidity = humidity;

View File

@ -1,5 +1,4 @@
using HAI_Shared;
using OmniLinkBridge.WebAPI;
using Serilog;
using System;
using System.Collections.Generic;

View File

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OmniLinkBridge.WebAPI
namespace OmniLinkBridge.WebAPI
{
public class OverrideZone
{

View File

@ -1,8 +1,7 @@
using System;
using System.Text;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OmniLinkBridge;
using OmniLinkBridge.MQTT;
using System.Collections.Generic;
namespace OmniLinkBridgeTest
{
@ -38,6 +37,64 @@ namespace OmniLinkBridgeTest
Assert.AreEqual(true, ((byte)3).IsBitSet(1));
}
[TestMethod]
public void TestToCommandCode()
{
string payload;
AreaCommandCode parser;
payload = "disarm";
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";
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);
// Special case for Home Assistant when code not required
payload = "disarm,validate,None";
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);
// 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]
public void TestParseRange()
{
@ -48,4 +105,4 @@ namespace OmniLinkBridgeTest
CollectionAssert.AreEqual(new List<int>(new int[] { 1, 2, 3, 5, 6 }), range);
}
}
}
}

View File

@ -1,10 +1,13 @@
using System;
using System.Text;
using System.Collections.Generic;
using HAI_Shared;
using HAI_Shared;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OmniLinkBridge;
using OmniLinkBridge.Modules;
using OmniLinkBridge.MQTT;
using OmniLinkBridgeTest.Mock;
using Serilog;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
namespace OmniLinkBridgeTest
{
@ -17,8 +20,26 @@ namespace OmniLinkBridgeTest
[TestInitialize]
public void Initialize()
{
string log_format = "{Timestamp:yyyy-MM-dd HH:mm:ss} [{SourceContext} {Level:u3}] {Message:lj}{NewLine}{Exception}";
var log_config = new LoggerConfiguration()
.MinimumLevel.Verbose()
.Enrich.FromLogContext()
.WriteTo.Console(outputTemplate: log_format);
Log.Logger = log_config.CreateLogger();
Dictionary<string, int> audioSources = new Dictionary<string, int>
{
{ "Radio", 1 },
{ "Streaming", 2 },
{ "TV", 4 }
};
omniLink = new MockOmniLinkII();
messageProcessor = new MessageProcessor(omniLink);
messageProcessor = new MessageProcessor(omniLink, audioSources, 8);
omniLink.Controller.Units[395].Type = enuOL2UnitType.Flag;
}
[TestMethod]
@ -39,7 +60,7 @@ namespace OmniLinkBridgeTest
[TestMethod]
public void AreaCommand()
{
void check(ushort id, string payload, enuUnitCommand command)
void check(ushort id, int code, string payload, enuUnitCommand command)
{
SendCommandEventArgs actual = null;
omniLink.OnSendCommand += (sender, e) => { actual = e; };
@ -47,29 +68,35 @@ namespace OmniLinkBridgeTest
SendCommandEventArgs expected = new SendCommandEventArgs()
{
Cmd = command,
Par = 0,
Par = (byte)code,
Pr2 = id
};
Assert.AreEqual(expected, actual);
}
// First area standard format
check(1, "disarm", enuUnitCommand.SecurityOff);
check(1, "arm_home", enuUnitCommand.SecurityDay);
check(1, "arm_away", enuUnitCommand.SecurityAway);
check(1, "arm_night", enuUnitCommand.SecurityNight);
check(1, "arm_home_instant", enuUnitCommand.SecurityDyi);
check(1, "arm_night_delay", enuUnitCommand.SecurityNtd);
check(1, "arm_vacation", enuUnitCommand.SecurityVac);
// Standard format
check(1, 0, "disarm", enuUnitCommand.SecurityOff);
check(1, 0, "arm_home", enuUnitCommand.SecurityDay);
check(1, 0, "arm_away", enuUnitCommand.SecurityAway);
check(1, 0, "arm_night", enuUnitCommand.SecurityNight);
check(1, 0, "arm_home_instant", enuUnitCommand.SecurityDyi);
check(1, 0, "arm_night_delay", enuUnitCommand.SecurityNtd);
check(1, 0, "arm_vacation", enuUnitCommand.SecurityVac);
// Last area with case check
check(8, "DISARM", enuUnitCommand.SecurityOff);
// Check all areas
check(0, 0, "disarm", enuUnitCommand.SecurityOff);
// Check with optional code
check(1, 1, "disarm,1", enuUnitCommand.SecurityOff);
// Check case insensitivity
check(8, 0, "DISARM", enuUnitCommand.SecurityOff);
}
[TestMethod]
public void ZoneCommand()
{
void check(ushort id, string payload, enuUnitCommand command)
void check(ushort id, int code, string payload, enuUnitCommand command, bool ensureNull = false)
{
SendCommandEventArgs actual = null;
omniLink.OnSendCommand += (sender, e) => { actual = e; };
@ -77,16 +104,31 @@ namespace OmniLinkBridgeTest
SendCommandEventArgs expected = new SendCommandEventArgs()
{
Cmd = command,
Par = 0,
Par = (byte)code,
Pr2 = id
};
Assert.AreEqual(expected, actual);
if (ensureNull)
Assert.IsNull(actual);
else
Assert.AreEqual(expected, actual);
}
check(1, "bypass", enuUnitCommand.Bypass);
check(1, "restore", enuUnitCommand.Restore);
// Standard format
check(1, 0, "bypass", enuUnitCommand.Bypass);
check(1, 0, "restore", enuUnitCommand.Restore);
check(2, "BYPASS", enuUnitCommand.Bypass);
// Check all zones
check(0, 0, "restore", enuUnitCommand.Restore);
// Not allowed to bypass all zones
check(0, 0, "bypass", enuUnitCommand.Bypass, true);
// Check with optional code
check(1, 1, "bypass,1", enuUnitCommand.Bypass);
// Check case insensitivity
check(2, 0, "BYPASS", enuUnitCommand.Bypass);
}
[TestMethod]
@ -113,6 +155,28 @@ namespace OmniLinkBridgeTest
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]
public void UnitLevelCommand()
{
@ -242,6 +306,140 @@ namespace OmniLinkBridgeTest
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);
}
[TestMethod]
public void AudioCommand()
{
void check(ushort id, string payload, enuUnitCommand command, int value)
{
SendCommandEventArgs actual = null;
omniLink.OnSendCommand += (sender, e) => { actual = e; };
messageProcessor.Process($"omnilink/audio{id}/command", payload);
SendCommandEventArgs expected = new SendCommandEventArgs()
{
Cmd = command,
Par = (byte)value,
Pr2 = id
};
Assert.AreEqual(expected, actual);
}
check(1, "ON", enuUnitCommand.AudioZone, 1);
check(1, "OFF", enuUnitCommand.AudioZone, 0);
check(2, "on", enuUnitCommand.AudioZone, 1);
}
[TestMethod]
public void AudioMuteCommand()
{
void check(ushort id, string payload, enuUnitCommand command, int value)
{
SendCommandEventArgs actual = null;
omniLink.OnSendCommand += (sender, e) => { actual = e; };
messageProcessor.Process($"omnilink/audio{id}/mute_command", payload);
SendCommandEventArgs expected = new SendCommandEventArgs()
{
Cmd = command,
Par = (byte)value,
Pr2 = id
};
Assert.AreEqual(expected, actual);
}
check(1, "ON", enuUnitCommand.AudioZone, 3);
check(1, "OFF", enuUnitCommand.AudioZone, 2);
Global.mqtt_audio_local_mute = true;
omniLink.Controller.AudioZones[2].Volume = 50;
check(2, "on", enuUnitCommand.AudioVolume, 0);
check(2, "off", enuUnitCommand.AudioVolume, 50);
omniLink.Controller.AudioZones[2].Volume = 0;
check(2, "on", enuUnitCommand.AudioVolume, 0);
check(2, "off", enuUnitCommand.AudioVolume, 10);
}
[TestMethod]
public void AudioSourceCommand()
{
void check(ushort id, string payload, enuUnitCommand command, int value)
{
SendCommandEventArgs actual = null;
omniLink.OnSendCommand += (sender, e) => { actual = e; };
messageProcessor.Process($"omnilink/audio{id}/source_command", payload);
SendCommandEventArgs expected = new SendCommandEventArgs()
{
Cmd = command,
Par = (byte)value,
Pr2 = id
};
Assert.AreEqual(expected, actual);
}
check(1, "Radio", enuUnitCommand.AudioSource, 1);
check(1, "Streaming", enuUnitCommand.AudioSource, 2);
check(2, "TV", enuUnitCommand.AudioSource, 4);
}
[TestMethod]
public void AudioVolumeCommand()
{
void check(ushort id, string payload, enuUnitCommand command, int value)
{
SendCommandEventArgs actual = null;
omniLink.OnSendCommand += (sender, e) => { actual = e; };
messageProcessor.Process($"omnilink/audio{id}/volume_command", payload);
SendCommandEventArgs expected = new SendCommandEventArgs()
{
Cmd = command,
Par = (byte)value,
Pr2 = id
};
Assert.AreEqual(expected, actual);
}
check(1, "100", enuUnitCommand.AudioVolume, 100);
check(1, "75", enuUnitCommand.AudioVolume, 75);
check(2, "0", enuUnitCommand.AudioVolume, 0);
Global.mqtt_audio_volume_media_player = true;
check(2, "1", enuUnitCommand.AudioVolume, 100);
check(2, "0.75", enuUnitCommand.AudioVolume, 75);
check(2, "0", enuUnitCommand.AudioVolume, 0);
}
}
}

View File

@ -1,15 +1,15 @@
using HAI_Shared;
using OmniLinkBridge.OmniLink;
using Serilog;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reflection;
namespace OmniLinkBridgeTest.Mock
{
class MockOmniLinkII : IOmniLinkII
{
private static readonly ILogger log = Log.Logger.ForContext(MethodBase.GetCurrentMethod().DeclaringType);
public clsHAC Controller { get; private set; }
public event EventHandler<SendCommandEventArgs> OnSendCommand;
@ -25,6 +25,7 @@ namespace OmniLinkBridgeTest.Mock
public bool SendCommand(enuUnitCommand Cmd, byte Par, ushort Pr2)
{
log.Verbose("Sending: {command}, Par1: {par1}, Par2: {par2}", Cmd, Par, Pr2);
OnSendCommand?.Invoke(null, new SendCommandEventArgs() { Cmd = Cmd, Par = Par, Pr2 = Pr2 });
return true;
}

View File

@ -1,9 +1,5 @@
using HAI_Shared;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OmniLinkBridgeTest.Mock
{
@ -13,6 +9,18 @@ namespace OmniLinkBridgeTest.Mock
public byte Par;
public ushort Pr2;
public SendCommandEventArgs()
{
}
public SendCommandEventArgs(enuUnitCommand cmd, byte par, ushort pr2)
{
Cmd = cmd;
Par = par;
Pr2 = pr2;
}
public override bool Equals(object other)
{
if (!(other is SendCommandEventArgs toCompareWith))

View File

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

View File

@ -9,7 +9,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>OmniLinkBridgeTest</RootNamespace>
<AssemblyName>OmniLinkBridgeTest</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">15.0</VisualStudioVersion>
@ -59,10 +59,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="MSTest.TestAdapter">
<Version>2.0.0</Version>
<Version>2.2.8</Version>
</PackageReference>
<PackageReference Include="MSTest.TestFramework">
<Version>2.0.0</Version>
<Version>2.2.8</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>

View File

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

View File

@ -1,9 +1,9 @@
using System;
using System.Text;
using System.Collections.Generic;
using System.Collections.Concurrent;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OmniLinkBridge;
using OmniLinkBridge.MQTT.HomeAssistant;
using System;
using System.Collections.Generic;
using ha = OmniLinkBridge.MQTT.HomeAssistant;
namespace OmniLinkBridgeTest
{
@ -37,8 +37,8 @@ namespace OmniLinkBridgeTest
Settings.LoadSettings(lines.ToArray());
Assert.AreEqual("1.1.1.1", Global.controller_address);
Assert.AreEqual(4369, Global.controller_port);
Assert.AreEqual("00-00-00-00-00-00-00-01", Global.controller_key1);
Assert.AreEqual("00-00-00-00-00-00-00-02", Global.controller_key2);
Assert.AreEqual("0000000000000001", Global.controller_key1);
Assert.AreEqual("0000000000000002", Global.controller_key2);
Assert.AreEqual("MyController", Global.controller_name);
}
@ -79,7 +79,8 @@ namespace OmniLinkBridgeTest
"verbose_thermostat_timer",
"verbose_thermostat",
"verbose_unit",
"verbose_message"
"verbose_message",
"verbose_lock"
})
{
List<string> lines = new List<string>(RequiredSettings())
@ -156,8 +157,12 @@ namespace OmniLinkBridgeTest
"mqtt_discovery_name_prefix = mynameprefix",
"mqtt_discovery_ignore_zones = 1,2-3,4",
"mqtt_discovery_ignore_units = 2-5,7",
"mqtt_discovery_override_area = id=1",
"mqtt_discovery_override_area = id=2;code_arm=true;code_disarm=true;arm_home=false;arm_away=false;arm_night=false;arm_vacation=false",
"mqtt_discovery_override_zone = id=5;device_class=garage_door",
"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());
Assert.AreEqual("myuser", Global.mqtt_username);
@ -168,10 +173,29 @@ namespace OmniLinkBridgeTest
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 }));
Dictionary<int, OmniLinkBridge.MQTT.OverrideArea> override_area = new Dictionary<int, OmniLinkBridge.MQTT.OverrideArea>()
{
{ 1, new OmniLinkBridge.MQTT.OverrideArea { }},
{ 2, new OmniLinkBridge.MQTT.OverrideArea { code_arm = true, code_disarm = true,
arm_home = false, arm_away = false, arm_night = false, arm_vacation = false }},
};
Assert.AreEqual(override_area.Count, Global.mqtt_discovery_override_area.Count);
foreach (KeyValuePair<int, OmniLinkBridge.MQTT.OverrideArea> pair in override_area)
{
Global.mqtt_discovery_override_area.TryGetValue(pair.Key, out OmniLinkBridge.MQTT.OverrideArea value);
Assert.AreEqual(override_area[pair.Key].code_arm, value.code_arm);
Assert.AreEqual(override_area[pair.Key].code_disarm, value.code_disarm);
Assert.AreEqual(override_area[pair.Key].arm_home, value.arm_home);
Assert.AreEqual(override_area[pair.Key].arm_away, value.arm_away);
Assert.AreEqual(override_area[pair.Key].arm_night, value.arm_night);
Assert.AreEqual(override_area[pair.Key].arm_vacation, value.arm_vacation);
}
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 }},
{ 7, new OmniLinkBridge.MQTT.OverrideZone { device_class = OmniLinkBridge.MQTT.BinarySensor.DeviceClass.motion }}
{ 5, new OmniLinkBridge.MQTT.OverrideZone { device_class = ha.BinarySensor.DeviceClass.garage_door }},
{ 7, new OmniLinkBridge.MQTT.OverrideZone { device_class = ha.BinarySensor.DeviceClass.motion }}
};
Assert.AreEqual(override_zone.Count, Global.mqtt_discovery_override_zone.Count);
@ -180,6 +204,30 @@ namespace OmniLinkBridgeTest
Global.mqtt_discovery_override_zone.TryGetValue(pair.Key, out OmniLinkBridge.MQTT.OverrideZone value);
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);
}
Assert.AreEqual(Global.mqtt_discovery_button_type, typeof(Switch));
// Test additional settings
lines.AddRange(new string[]
{
"mqtt_discovery_button_type = button"
});
Settings.LoadSettings(lines.ToArray());
Assert.AreEqual(Global.mqtt_discovery_button_type, typeof(Button));
}
[TestMethod]

View File

@ -1,21 +1,23 @@
# OmniLink Bridge
Provides MQTT bridge, web service API, time sync, and logging for [HAI/Leviton OmniPro II controllers](https://www.leviton.com/en/products/brands/omni-security-automation). Provides integration with [Samsung SmartThings via web service API](https://github.com/excaliburpartners/SmartThings-OmniPro) and [Home Assistant via MQTT](https://www.home-assistant.io/components/mqtt/).
Please note that OmniLink Bridge is not in active development. The MQTT and Home Assistant integrations are in maintenance mode. The SmartThings Web API and MySQL logging are deprecated and not feature consistent with MQTT.
## Download
You can use docker to build an image from git or download the [binary here](https://github.com/excaliburpartners/OmniLinkBridge/releases/latest/download/OmniLinkBridge.zip).
You can use docker to build an image from git or download the [binary here](https://github.com/excaliburpartners/OmniLinkBridge/releases/latest/download/OmniLinkBridge.zip). You can also install it as a [Home Assistant Add-on](https://github.com/excaliburpartners/hassio-addons).
## Requirements
- [Docker](https://www.docker.com/)
- .NET Framework 4.5.2 (or Mono equivalent)
- .NET Framework 4.7.2 (or Mono equivalent)
## 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_
- Maintains connection to the OmniLink controller
- Thermostats
- If no status update has been received after 4 minutes a request is issued
- A status update containing a temperature of 0 is ignored
- A status update containing a temperature of 0 marks the thermostat offline
- This can occur when a ZigBee thermostat has lost communication
- Time Sync: time_
- Controller time is checked and compared to the local computer time disregarding time zones
@ -28,7 +30,7 @@ OmniLink Bridge is divided into the following modules and configurable settings.
- Provides integration with [Samsung SmartThings](https://github.com/excaliburpartners/SmartThings-OmniPro)
- Allows an application to subscribe to receive POST notifications status updates are received from the OmniLinkII module
- On failure to POST to callback URL subscription is removed
- Recommended for application to send subscribe reqeusts every few minutes
- Recommended for application to send subscribe requests every few minutes
- Requests to GET endpoints return status from the OmniLinkII module
- Requests to POST endpoints send commands to the OmniLinkII module
- Logger
@ -146,6 +148,17 @@ systemctl start omnilinkbridge.service
```
## MQTT
```
SUB omnilink/status
string online, offline
SUB omnilink/model
string Controller model
SUB omnilink/version
string Controller version
```
### System
```
SUB omnilink/system/phone/state
@ -166,17 +179,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
string triggered, arming, armed_night, armed_home, armed_away, disarmed
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
@ -201,6 +217,8 @@ int Current relative humidity
PUB omnilink/zoneX/command
string bypass, restore
note Use zone 0 to restore all zones
note Optionally the user code number can be specified 'bypass,1'
```
### Units
@ -216,6 +234,10 @@ SUB omnilink/unitX/brightness_state
PUB omnilink/unitX/brightness_command
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
PUB omnilink/unitX/scene_command
string A-L
@ -226,6 +248,9 @@ string A-L
SUB omnilink/thermostatX/name
string Thermostat name
SUB omnilink/thermostatX/status
string online, offline
SUB omnilink/thermostatX/current_operation
string idle, cooling, heating
@ -249,15 +274,20 @@ int Setpoint in relative humidity
SUB omnilink/thermostatX/mode_state
PUB omnilink/thermostatX/mode_command
string auto, off, cool, heat, e_heat
SUB omnilink/thermostatX/mode_basic_state
string auto, off, cool, heat
SUB omnilink/thermostatX/fan_mode_state
PUB omnilink/thermostatX/fan_mode_command
string auto, on, cycle
SUB omnilink/thermostatX/hold_state
SUB omnilink/thermostatX/hold_state
string off, on, vacation
PUB omnilink/thermostatX/hold_command
string off, hold
string off, on
```
### Buttons
@ -284,6 +314,50 @@ PUB omnilink/messageX/command
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
```
### Audio Sources
```
SUB omnilink/sourceXX/name
string Audio source name
```
### Audio Zones
```
SUB omnilink/audioXX/name
string Audio zone name
SUB omnilink/audioXX/state
PUB omnilink/audioXX/command
string OFF, ON
note Use audio 0 to change all audio zones
SUB omnilink/audioXX/mute_state
PUB omnilink/audioXX/mute_command
string OFF, ON
note Use audio 0 to change all audio zones
SUB omnilink/audioXX/source_state
PUB omnilink/audioXX/source_command
string Source name
note Refer to omnilink/sourceXX/name
SUB omnilink/audioXX/volume_state
PUB omnilink/audioXX/volume_command
int Level from 0 to 100 percent
double Level from 0.00 to 1.00 (mqtt_audio_volume_media_player = yes)
```
## 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.
@ -348,7 +422,7 @@ POST /PushButton
```
## MySQL
The [MySQL ODBC Connector](http://dev.mysql.com/downloads/connector/odbc/) is required for MySQL logging. The docker image comes with the MySQL ODBC connector installed. For Windows and Linux you will need to download and install it.
The [MySQL ODBC Connector](http://dev.mysql.com/downloads/connector/odbc/) is required for MySQL logging. The docker image comes with the MySQL ODBC connector installed. For Windows and Linux you will need to download and install it. The Home Assistant Add-on does not support MySQL logging.
Configure mysql_connection in OmniLinkBridge.ini. For Windows change DRIVER={MySQL} to name of the driver shown in the ODBC Data Source Administrator.
```