From 336e02e8e85c4f50093fc864542cd8db3d802395 Mon Sep 17 00:00:00 2001 From: Ryan Wagoner Date: Sat, 14 Dec 2019 22:11:23 -0500 Subject: [PATCH] 1.1.5 - Update readme, fix thermostat logging interval, and cleanup code --- OmniLinkBridge/Modules/LoggerModule.cs | 31 ++--- OmniLinkBridge/Modules/MQTTModule.cs | 8 +- OmniLinkBridge/Modules/OmniLinkII.cs | 44 ++++--- OmniLinkBridge/Modules/TimeSyncModule.cs | 4 +- OmniLinkBridge/Modules/WebServiceModule.cs | 2 +- OmniLinkBridge/Properties/AssemblyInfo.cs | 4 +- OmniLinkBridge/Settings.cs | 58 +-------- OmniLinkBridge/WebService/OmniLinkService.cs | 2 +- OmniLinkBridge/WebService/WebNotification.cs | 4 +- README.md | 129 ++++++++++++++++--- 10 files changed, 167 insertions(+), 119 deletions(-) diff --git a/OmniLinkBridge/Modules/LoggerModule.cs b/OmniLinkBridge/Modules/LoggerModule.cs index 23e67b0..7dbac55 100644 --- a/OmniLinkBridge/Modules/LoggerModule.cs +++ b/OmniLinkBridge/Modules/LoggerModule.cs @@ -12,17 +12,17 @@ namespace OmniLinkBridge.Modules { public class LoggerModule : IModule { - private static ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + private static readonly ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); - private OmniLinkII omnilink; - private List alarms = new List(); + private readonly OmniLinkII omnilink; + private readonly List alarms = new List(); // mySQL Database private OdbcConnection mysql_conn = null; private DateTime mysql_retry = DateTime.MinValue; private OdbcCommand mysql_command = null; - private Queue mysql_queue = new Queue(); - private object mysql_lock = new object(); + private readonly Queue mysql_queue = new Queue(); + private readonly object mysql_lock = new object(); private readonly AutoResetEvent trigger = new AutoResetEvent(false); @@ -221,18 +221,14 @@ namespace OmniLinkBridge.Modules private void Omnilink_OnThermostatStatus(object sender, ThermostatStatusEventArgs e) { - if (e.EventTimer) - return; - - int temp, heat, cool, humidity, humidify, dehumidify; - - Int32.TryParse(e.Thermostat.TempText(), out temp); - Int32.TryParse(e.Thermostat.HeatSetpointText(), out heat); - Int32.TryParse(e.Thermostat.CoolSetpointText(), out cool); - Int32.TryParse(e.Thermostat.HumidityText(), out humidity); - Int32.TryParse(e.Thermostat.HumidifySetpointText(), out humidify); - Int32.TryParse(e.Thermostat.DehumidifySetpointText(), out dehumidify); + int.TryParse(e.Thermostat.TempText(), out int temp); + int.TryParse(e.Thermostat.HeatSetpointText(), out int heat); + int.TryParse(e.Thermostat.CoolSetpointText(), out int cool); + int.TryParse(e.Thermostat.HumidityText(), out int humidity); + int.TryParse(e.Thermostat.HumidifySetpointText(), out int humidify); + int.TryParse(e.Thermostat.DehumidifySetpointText(), out int dehumidify); + // Log all events including thermostat polling DBQueue(@" INSERT INTO log_thermostats (timestamp, id, name, status, temp, heat, cool, @@ -243,7 +239,8 @@ namespace OmniLinkBridge.Modules humidity + "','" + humidify + "','" + dehumidify + "','" + e.Thermostat.ModeText() + "','" + e.Thermostat.FanModeText() + "','" + e.Thermostat.HoldStatusText() + "')"); - if (Global.verbose_thermostat) + // Ignore events fired by thermostat polling + if (!e.EventTimer && Global.verbose_thermostat) log.Debug("ThermostatStatus " + e.ID + " " + e.Thermostat.Name + ", Status: " + e.Thermostat.TempText() + " " + e.Thermostat.HorC_StatusText() + ", Heat: " + e.Thermostat.HeatSetpointText() + diff --git a/OmniLinkBridge/Modules/MQTTModule.cs b/OmniLinkBridge/Modules/MQTTModule.cs index a9850a8..0402a73 100644 --- a/OmniLinkBridge/Modules/MQTTModule.cs +++ b/OmniLinkBridge/Modules/MQTTModule.cs @@ -18,14 +18,13 @@ namespace OmniLinkBridge.Modules { public class MQTTModule : IModule { - private static ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + private static readonly ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); public static DeviceRegistry MqttDeviceRegistry { get; set; } private OmniLinkII OmniLink { get; set; } private IManagedMqttClient MqttClient { get; set; } private bool ControllerConnected { get; set; } - private MessageProcessor MessageProcessor { get; set; } private readonly AutoResetEvent trigger = new AutoResetEvent(false); @@ -371,9 +370,8 @@ namespace OmniLinkBridge.Modules if (!MqttClient.IsConnected) return; - // Ignore events fired by thermostat polling and when temperature is invalid - // An invalid temperature can occur when a Zigbee thermostat is unreachable - if (!e.EventTimer && e.Thermostat.Temp > 0) + // Ignore events fired by thermostat polling + if (!e.EventTimer) PublishThermostatState(e.Thermostat); } diff --git a/OmniLinkBridge/Modules/OmniLinkII.cs b/OmniLinkBridge/Modules/OmniLinkII.cs index b5914dc..60ae8d5 100644 --- a/OmniLinkBridge/Modules/OmniLinkII.cs +++ b/OmniLinkBridge/Modules/OmniLinkII.cs @@ -12,16 +12,16 @@ namespace OmniLinkBridge.Modules { public class OmniLinkII : IModule, IOmniLinkII { - private static ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + private static readonly ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); // OmniLink Controller public clsHAC Controller { get; private set; } private DateTime retry = DateTime.MinValue; // Thermostats - private Dictionary tstats = new Dictionary(); - private System.Timers.Timer tstat_timer = new System.Timers.Timer(); - private object tstat_lock = new object(); + private readonly Dictionary tstats = new Dictionary(); + private readonly System.Timers.Timer tstat_timer = new System.Timers.Timer(); + private readonly object tstat_lock = new object(); // Events public event EventHandler OnConnect; @@ -382,13 +382,15 @@ namespace OmniLinkBridge.Modules private void GetNextNamed(enuObjectType type, int ix) { - clsOL2MsgRequestProperties MSG = new clsOL2MsgRequestProperties(Controller.Connection); - MSG.ObjectType = type; - MSG.IndexNumber = (UInt16)ix; - MSG.RelativeDirection = 1; // next object after IndexNumber - MSG.Filter1 = 1; // (0=Named or Unnamed, 1=Named, 2=Unnamed). - MSG.Filter2 = 0; // Any Area - MSG.Filter3 = 0; // Any Room + clsOL2MsgRequestProperties MSG = new clsOL2MsgRequestProperties(Controller.Connection) + { + ObjectType = type, + IndexNumber = (UInt16)ix, + RelativeDirection = 1, // next object after IndexNumber + Filter1 = 1, // (0=Named or Unnamed, 1=Named, 2=Unnamed). + Filter2 = 0, // Any Area + Filter3 = 0 // Any Room + }; Controller.Connection.Send(MSG, HandleNamedPropertiesResponse); } @@ -724,12 +726,19 @@ namespace OmniLinkBridge.Modules lock (tstat_lock) { Controller.Thermostats[MSG.ObjectNumber(i)].CopyExtendedStatus(MSG, i); - OnThermostatStatus?.Invoke(this, new ThermostatStatusEventArgs() + + // Don't fire event when invalid temperature of 0 is sometimes received + if (Controller.Thermostats[MSG.ObjectNumber(i)].Temp > 0) { - ID = MSG.ObjectNumber(i), - Thermostat = Controller.Thermostats[MSG.ObjectNumber(i)], - EventTimer = false - }); + OnThermostatStatus?.Invoke(this, new ThermostatStatusEventArgs() + { + ID = MSG.ObjectNumber(i), + Thermostat = Controller.Thermostats[MSG.ObjectNumber(i)], + EventTimer = false + }); + } + else if (Global.verbose_thermostat_timer) + log.Warn("Ignoring unsolicited unknown temp for Thermostat " + Controller.Thermostats[MSG.ObjectNumber(i)].Name); if (!tstats.ContainsKey(MSG.ObjectNumber(i))) tstats.Add(MSG.ObjectNumber(i), DateTime.Now); @@ -809,6 +818,7 @@ 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() @@ -819,7 +829,7 @@ namespace OmniLinkBridge.Modules }); } else if (Global.verbose_thermostat_timer) - log.Warn("Not logging unknown temp for Thermostat " + Controller.Thermostats[tstat.Key].Name); + log.Warn("Ignoring unknown temp for Thermostat " + Controller.Thermostats[tstat.Key].Name); } else if (Global.verbose_thermostat_timer) log.Warn("Not logging out of date status for Thermostat " + Controller.Thermostats[tstat.Key].Name); diff --git a/OmniLinkBridge/Modules/TimeSyncModule.cs b/OmniLinkBridge/Modules/TimeSyncModule.cs index 44b820d..92488f3 100644 --- a/OmniLinkBridge/Modules/TimeSyncModule.cs +++ b/OmniLinkBridge/Modules/TimeSyncModule.cs @@ -8,11 +8,11 @@ namespace OmniLinkBridge.Modules { public class TimeSyncModule : IModule { - private static ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + private static readonly ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); private OmniLinkII OmniLink { get; set; } - private System.Timers.Timer tsync_timer = new System.Timers.Timer(); + private readonly System.Timers.Timer tsync_timer = new System.Timers.Timer(); private DateTime tsync_check = DateTime.MinValue; private readonly AutoResetEvent trigger = new AutoResetEvent(false); diff --git a/OmniLinkBridge/Modules/WebServiceModule.cs b/OmniLinkBridge/Modules/WebServiceModule.cs index 925a4ad..392a4e2 100644 --- a/OmniLinkBridge/Modules/WebServiceModule.cs +++ b/OmniLinkBridge/Modules/WebServiceModule.cs @@ -14,7 +14,7 @@ namespace OmniLinkBridge { public class WebServiceModule : IModule { - private static ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + private static readonly ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); public static OmniLinkII OmniLink { get; private set; } diff --git a/OmniLinkBridge/Properties/AssemblyInfo.cs b/OmniLinkBridge/Properties/AssemblyInfo.cs index 7eda995..72d441b 100644 --- a/OmniLinkBridge/Properties/AssemblyInfo.cs +++ b/OmniLinkBridge/Properties/AssemblyInfo.cs @@ -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.4.0")] -[assembly: AssemblyFileVersion("1.1.4.0")] +[assembly: AssemblyVersion("1.1.5.0")] +[assembly: AssemblyFileVersion("1.1.5.0")] diff --git a/OmniLinkBridge/Settings.cs b/OmniLinkBridge/Settings.cs index b9d46c4..6bd651f 100644 --- a/OmniLinkBridge/Settings.cs +++ b/OmniLinkBridge/Settings.cs @@ -13,7 +13,7 @@ namespace OmniLinkBridge { public static class Settings { - private static ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + private static readonly ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); public static void LoadSettings() { @@ -60,7 +60,7 @@ namespace OmniLinkBridge Global.mqtt_discovery_name_prefix = settings["mqtt_discovery_name_prefix"] ?? string.Empty; if (!string.IsNullOrEmpty(Global.mqtt_discovery_name_prefix)) - Global.mqtt_discovery_name_prefix = Global.mqtt_discovery_name_prefix + " "; + Global.mqtt_discovery_name_prefix += " "; Global.mqtt_discovery_ignore_zones = ValidateRange(settings, "mqtt_discovery_ignore_zones"); Global.mqtt_discovery_ignore_units = ValidateRange(settings, "mqtt_discovery_ignore_units"); @@ -103,7 +103,7 @@ namespace OmniLinkBridge .Select(s => s.Split('=')) .ToDictionary(a => a[0].Trim(), a => a[1].Trim(), StringComparer.InvariantCultureIgnoreCase); - if (!attributes.ContainsKey("id") || !Int32.TryParse(attributes["id"], out int attrib_id)) + if (!attributes.ContainsKey("id") || !int.TryParse(attributes["id"], out int attrib_id)) throw new Exception("Missing or invalid id attribute"); T override_zone = new T(); @@ -139,7 +139,7 @@ namespace OmniLinkBridge { try { - return Int32.Parse(settings[section]); + return int.Parse(settings[section]); } catch { @@ -165,7 +165,7 @@ namespace OmniLinkBridge { try { - int port = Int32.Parse(settings[section]); + int port = int.Parse(settings[section]); if (port < 1 || port > 65534) throw new Exception(); @@ -179,54 +179,6 @@ namespace OmniLinkBridge } } - private static bool ValidateBool(NameValueCollection settings, string section) - { - try - { - return Boolean.Parse(settings[section]); - } - catch - { - log.Error("Invalid bool specified for " + section); - throw; - } - } - - private static IPAddress ValidateIP(NameValueCollection settings, string section) - { - if (settings[section] == "*") - return IPAddress.Any; - - if (settings[section] == "") - return IPAddress.None; - - try - { - return IPAddress.Parse(section); - } - catch - { - log.Error("Invalid IP specified for " + section); - throw; - } - } - - private static string ValidateDirectory(NameValueCollection settings, string section) - { - try - { - if (!Directory.Exists(settings[section])) - Directory.CreateDirectory(settings[section]); - - return settings[section]; - } - catch - { - log.Error("Invalid directory specified for " + section); - throw; - } - } - private static MailAddress ValidateMailFrom(NameValueCollection settings, string section) { try diff --git a/OmniLinkBridge/WebService/OmniLinkService.cs b/OmniLinkBridge/WebService/OmniLinkService.cs index a007806..fdb15af 100644 --- a/OmniLinkBridge/WebService/OmniLinkService.cs +++ b/OmniLinkBridge/WebService/OmniLinkService.cs @@ -12,7 +12,7 @@ namespace OmniLinkBridge.WebAPI [ServiceBehavior(IncludeExceptionDetailInFaults = true)] public class OmniLinkService : IOmniLinkService { - private static ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + private static readonly ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); public void Subscribe(SubscribeContract contract) { diff --git a/OmniLinkBridge/WebService/WebNotification.cs b/OmniLinkBridge/WebService/WebNotification.cs index 5f90fc7..d16600a 100644 --- a/OmniLinkBridge/WebService/WebNotification.cs +++ b/OmniLinkBridge/WebService/WebNotification.cs @@ -10,10 +10,10 @@ namespace OmniLinkBridge.WebAPI { static class WebNotification { - private static ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + private static readonly ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); private static List subscriptions = new List(); - private static object subscriptions_lock = new object(); + private static readonly object subscriptions_lock = new object(); public static void AddSubscription(string callback) { diff --git a/README.md b/README.md index 36503c5..4260728 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,59 @@ # 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 SmarthThings via web service API](https://github.com/excaliburpartners/SmartThings-OmniPro) and [Home Assistant via MQTT](https://www.home-assistant.io/components/mqtt/). +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/). ## Download -You can use docker to build an image from git or download the [binary here](http://www.excalibur-partners.com/downloads/OmniLinkBridge_1_1_4.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). ## Requirements - [Docker](https://www.docker.com/) - .NET Framework 4.5.2 (or Mono equivalent) ## Operation -- Area, Messages, Units, and Zones are logged to mySQL when status changes -- Thermostats are logged to mySQL once per minute - - If no notifications are received within 4 minutes a request is issued - - After 5 minutes of no updates a warning will be logged and mySQL will not be updated - - If the temp is 0 a warning will be logged and mySQL will not be updated -- Controller time is checked and compared to the local computer time disregarding time zones +OmniLinkBridge is divided into the following modules -## Notifications -- Supports email, prowl, and pushover -- Always sent for area alarms and critical system events -- Optionally enable for area status changes and console messages +- OmniLinkII + - Settings: 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 + - This can occur when a ZigBee thermostat has lost communication +- Logger + - Console output + - Settings: verbose_ + - Thermostats (verbose_thermostat_timer) + - After 5 minutes of no status updates a warning will be logged + - When a current temperature of 0 is received a warning will be logged + - MySQL logging + - Settings: mysql_ + - Thermostats are logged every minute and when an event is received + - Push notifications + - Settings: notify_ + - Always sent for area alarms and critical system events + - Optionally enable for area status changes and console messages + - Email + - Settings: mail_ + - Prowl + - Settings: prowl_ + - Pushover + - Settings: pushover_ +- Time Sync + - Settings: time_ + - Controller time is checked and compared to the local computer time disregarding time zones +- Web API + - Settings: webapi_ + - 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 + - Requests to GET endpoints return status from the OmniLinkII module + - Requests to POST endpoints send commands to the OmniLinkII module +- MQTT + - Settings: mqtt_ + - Maintains connection to the MQTT broker + - Publishes discovery topics for [Home Assistant](https://www.home-assistant.io/components/mqtt/) to auto configure devices + - Publishes topics for status received from the OmniLinkII module + - Subscribes to command topics and sends commands to the OmniLinkII module ## Docker Hub (preferred) 1. Configure at a minimum the controller IP and encryptions keys. The web service port must be 8000. @@ -62,7 +96,7 @@ docker logs omnilink-bridge ## Installation Windows 1. Copy files to your desired location like C:\OmniLinkBridge 2. Edit OmniLinkBridge.ini and define at a minimum the controller IP and encryptions keys -3. Run OmniLinkBridge.exe to verify connectivity +3. Run OmniLinkBridge.exe from the command prompt to verify connectivity 4. Add Windows service ``` sc create OmniLinkBridge binpath=C:\OmniLinkBridge\OmniLinkBridge.exe @@ -95,7 +129,6 @@ systemctl start omnilinkbridge.service ``` ## MQTT -This module will also publish discovery topics for Home Assistant to auto configure devices. ### Areas ``` @@ -183,16 +216,69 @@ PUB omnilink/buttonX/command string ON ``` -## Web Service API -To test the API you can use your browser to view a page or PowerShell (see below) to change a value. - -- http://localhost:8000/ListUnits -- http://localhost:8000/GetUnit?id=1 +## 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. ``` Invoke-WebRequest -Uri "http://localhost:8000/SetUnit" -Method POST -ContentType "application/json" -Body (convertto-json -InputObject @{"id"=1;"value"=100}) -UseBasicParsing ``` +### Subscription +``` +POST /Subscribe +{ "callback": url } +Callback is a POST request with Type header added and json body identical to the related /Get +Type: area, contact, motion, water, smoke, co, temp, unit, thermostat +``` + +### Areas +``` +GET /ListAreas +GET /GetArea?id=X +``` + +### Zones +``` +GET /ListZonesContact +GET /ListZonesMotion +GET /ListZonesWater +GET /ListZonesSmoke +GET /ListZonesCO +GET /ListZonesTemp +GET /GetZone?id=X +``` + +### Units +``` +GET /ListUnits +GET /GetZone?id=X +POST /SetUnit +POST /SetUnitKeypadPress +{ "id":X, "value":0-100 } +``` + +### Thermostats +``` +GET /ListThermostats +GET /GetThermostat?id=X +POST /SetThermostatCoolSetpoint +POST /SetThermostatHeatSetpoint +POST /SetThermostatMode +POST /SetThermostatFanMode +POST /SetThermostatHold +{ "id":X, "value": } +int mode 0=off, 1=heat, 2=cool, 3=auto, 4=emergency heat +int fanmode 0=auto, 1=on, 2=circulate +int hold 0=off, 1=on +``` + +### Thermostats +``` +GET /ListButtons +POST /PushButton +{ "id":X, "value":1 } +``` + ## MySQL Setup You will want to install the MySQL Community Server, Workbench, and ODBC Connector. The Workbench software provides a graphical interface to administer the MySQL server. The OmniLink Bridge uses ODBC to communicate with the database. The MySQL ODBC Connector library is needed for Windows ODBC to communicate with MySQL. @@ -215,6 +301,11 @@ mysql_connection = DRIVER={MySQL};SERVER=localhost;DATABASE=OmniLinkBridge;USER= ``` ## Change Log +Version 1.1.5 - 2019-12-14 +- Fix SQL logging for areas, units, and thermostats +- Refactor MQTT parser and add unit tests +- Update readme, fix thermostat logging interval, and cleanup code + Version 1.1.4 - 2019-11-22 - Utilize controller temperature format - Don't publish invalid thermostat temperatures