From 4e2bb856231f1c0b4e71ae644c71b403b8c1f8c3 Mon Sep 17 00:00:00 2001 From: Ryan Wagoner Date: Sat, 13 Oct 2018 17:28:47 -0400 Subject: [PATCH] 1.1.0 - Renamed to OmniLinkBridge - Restructured code to be event based with modules - Added MQTT module for Home Assistant - Added pushover notifications - Added web service API subscriptions file to persist subscriptions --- .gitignore | 4 + HAILogger.sln | 22 - HAILogger/App.config | 16 - HAILogger/CoreServer.cs | 1245 ----------------- HAILogger/Event.cs | 165 --- HAILogger/Global.cs | 54 - HAILogger/HAILogger.csproj | 107 -- HAILogger/HAILogger.ini | 55 - HAILogger/Prowl.cs | 50 - HAILogger/WebNotification.cs | 60 - HAILogger/WebService.cs | 44 - OmniLinkBridge.sln | 31 + OmniLinkBridge/App.config | 37 + OmniLinkBridge/CoreServer.cs | 75 + OmniLinkBridge/Extensions.cs | 45 + OmniLinkBridge/Global.cs | 75 + .../HAI.Controller.dll | Bin OmniLinkBridge/MQTT/Alarm.cs | 15 + OmniLinkBridge/MQTT/BinarySensor.cs | 30 + OmniLinkBridge/MQTT/Climate.cs | 30 + OmniLinkBridge/MQTT/Device.cs | 17 + OmniLinkBridge/MQTT/Light.cs | 19 + OmniLinkBridge/MQTT/MappingExtensions.cs | 223 +++ OmniLinkBridge/MQTT/OverrideZone.cs | 13 + OmniLinkBridge/MQTT/Sensor.cs | 26 + OmniLinkBridge/MQTT/Switch.cs | 13 + OmniLinkBridge/MQTT/Topics.cs | 56 + OmniLinkBridge/Modules/IModule.cs | 8 + OmniLinkBridge/Modules/LoggerModule.cs | 344 +++++ OmniLinkBridge/Modules/MQTTModule.cs | 422 ++++++ OmniLinkBridge/Modules/OmniLinkII.cs | 837 +++++++++++ OmniLinkBridge/Modules/TimeSyncModule.cs | 111 ++ OmniLinkBridge/Modules/WebServiceModule.cs | 119 ++ .../Notifications/EmailNotification.cs | 43 + OmniLinkBridge/Notifications/INotification.cs | 7 + OmniLinkBridge/Notifications/Notification.cs | 23 + .../Notifications/NotificationPriority.cs | 30 + .../Notifications/ProwlNotification.cs | 42 + .../Notifications/PushoverNotification.cs | 41 + .../OmniLink/AreaStatusEventArgs.cs | 11 + .../OmniLink/MessageStatusEventArgs.cs | 11 + .../OmniLink/SystemStatusEventArgs.cs | 12 + .../OmniLink/ThermostatStatusEventArgs.cs | 12 + .../OmniLink/UnitStatusEventArgs.cs | 11 + .../OmniLink/ZoneStatusEventArgs.cs | 11 + OmniLinkBridge/OmniLinkBridge.csproj | 170 +++ OmniLinkBridge/OmniLinkBridge.ini | 72 + {HAILogger => OmniLinkBridge}/Program.cs | 24 +- .../ProjectInstaller.Designer.cs | 4 +- .../ProjectInstaller.cs | 2 +- .../ProjectInstaller.resx | 0 .../Properties/AssemblyInfo.cs | 10 +- .../Service.Designer.cs | 4 +- {HAILogger => OmniLinkBridge}/Service.cs | 2 +- {HAILogger => OmniLinkBridge}/Service.resx | 0 {HAILogger => OmniLinkBridge}/Settings.cs | 162 ++- .../WebService}/AreaContract.cs | 2 +- .../WebService}/CommandContract.cs | 2 +- .../WebService/IOmniLinkService.cs | 4 +- .../WebService/MappingExtensions.cs | 46 +- .../WebService}/NameContract.cs | 2 +- .../WebService/OmniLinkService.cs | 134 +- .../WebService}/SubscribeContract.cs | 2 +- .../WebService}/ThermostatContract.cs | 2 +- .../WebService}/UnitContract.cs | 2 +- OmniLinkBridge/WebService/WebNotification.cs | 107 ++ .../WebService}/ZoneContract.cs | 2 +- OmniLinkBridgeTest/AssemblyTestHarness.cs | 19 + OmniLinkBridgeTest/ExtensionTest.cs | 22 + OmniLinkBridgeTest/NotificationTest.cs | 18 + OmniLinkBridgeTest/OmniLinkBridgeTest.csproj | 67 + OmniLinkBridgeTest/Properties/AssemblyInfo.cs | 20 + README.md | 144 +- 73 files changed, 3635 insertions(+), 2032 deletions(-) create mode 100644 .gitignore delete mode 100644 HAILogger.sln delete mode 100644 HAILogger/App.config delete mode 100644 HAILogger/CoreServer.cs delete mode 100644 HAILogger/Event.cs delete mode 100644 HAILogger/Global.cs delete mode 100644 HAILogger/HAILogger.csproj delete mode 100644 HAILogger/HAILogger.ini delete mode 100644 HAILogger/Prowl.cs delete mode 100644 HAILogger/WebNotification.cs delete mode 100644 HAILogger/WebService.cs create mode 100644 OmniLinkBridge.sln create mode 100644 OmniLinkBridge/App.config create mode 100644 OmniLinkBridge/CoreServer.cs create mode 100644 OmniLinkBridge/Extensions.cs create mode 100644 OmniLinkBridge/Global.cs rename {HAILogger => OmniLinkBridge}/HAI.Controller.dll (100%) create mode 100644 OmniLinkBridge/MQTT/Alarm.cs create mode 100644 OmniLinkBridge/MQTT/BinarySensor.cs create mode 100644 OmniLinkBridge/MQTT/Climate.cs create mode 100644 OmniLinkBridge/MQTT/Device.cs create mode 100644 OmniLinkBridge/MQTT/Light.cs create mode 100644 OmniLinkBridge/MQTT/MappingExtensions.cs create mode 100644 OmniLinkBridge/MQTT/OverrideZone.cs create mode 100644 OmniLinkBridge/MQTT/Sensor.cs create mode 100644 OmniLinkBridge/MQTT/Switch.cs create mode 100644 OmniLinkBridge/MQTT/Topics.cs create mode 100644 OmniLinkBridge/Modules/IModule.cs create mode 100644 OmniLinkBridge/Modules/LoggerModule.cs create mode 100644 OmniLinkBridge/Modules/MQTTModule.cs create mode 100644 OmniLinkBridge/Modules/OmniLinkII.cs create mode 100644 OmniLinkBridge/Modules/TimeSyncModule.cs create mode 100644 OmniLinkBridge/Modules/WebServiceModule.cs create mode 100644 OmniLinkBridge/Notifications/EmailNotification.cs create mode 100644 OmniLinkBridge/Notifications/INotification.cs create mode 100644 OmniLinkBridge/Notifications/Notification.cs create mode 100644 OmniLinkBridge/Notifications/NotificationPriority.cs create mode 100644 OmniLinkBridge/Notifications/ProwlNotification.cs create mode 100644 OmniLinkBridge/Notifications/PushoverNotification.cs create mode 100644 OmniLinkBridge/OmniLink/AreaStatusEventArgs.cs create mode 100644 OmniLinkBridge/OmniLink/MessageStatusEventArgs.cs create mode 100644 OmniLinkBridge/OmniLink/SystemStatusEventArgs.cs create mode 100644 OmniLinkBridge/OmniLink/ThermostatStatusEventArgs.cs create mode 100644 OmniLinkBridge/OmniLink/UnitStatusEventArgs.cs create mode 100644 OmniLinkBridge/OmniLink/ZoneStatusEventArgs.cs create mode 100644 OmniLinkBridge/OmniLinkBridge.csproj create mode 100644 OmniLinkBridge/OmniLinkBridge.ini rename {HAILogger => OmniLinkBridge}/Program.cs (77%) rename {HAILogger => OmniLinkBridge}/ProjectInstaller.Designer.cs (94%) rename {HAILogger => OmniLinkBridge}/ProjectInstaller.cs (93%) rename {HAILogger => OmniLinkBridge}/ProjectInstaller.resx (100%) rename {HAILogger => OmniLinkBridge}/Properties/AssemblyInfo.cs (87%) rename {HAILogger => OmniLinkBridge}/Service.Designer.cs (93%) rename {HAILogger => OmniLinkBridge}/Service.cs (96%) rename {HAILogger => OmniLinkBridge}/Service.resx (100%) rename {HAILogger => OmniLinkBridge}/Settings.cs (56%) rename {HAILogger => OmniLinkBridge/WebService}/AreaContract.cs (94%) rename {HAILogger => OmniLinkBridge/WebService}/CommandContract.cs (87%) rename HAILogger/IHAIService.cs => OmniLinkBridge/WebService/IOmniLinkService.cs (98%) rename HAILogger/Helper.cs => OmniLinkBridge/WebService/MappingExtensions.cs (66%) rename {HAILogger => OmniLinkBridge/WebService}/NameContract.cs (87%) rename HAILogger/HAIService.cs => OmniLinkBridge/WebService/OmniLinkService.cs (58%) rename {HAILogger => OmniLinkBridge/WebService}/SubscribeContract.cs (84%) rename {HAILogger => OmniLinkBridge/WebService}/ThermostatContract.cs (96%) rename {HAILogger => OmniLinkBridge/WebService}/UnitContract.cs (90%) create mode 100644 OmniLinkBridge/WebService/WebNotification.cs rename {HAILogger => OmniLinkBridge/WebService}/ZoneContract.cs (93%) create mode 100644 OmniLinkBridgeTest/AssemblyTestHarness.cs create mode 100644 OmniLinkBridgeTest/ExtensionTest.cs create mode 100644 OmniLinkBridgeTest/NotificationTest.cs create mode 100644 OmniLinkBridgeTest/OmniLinkBridgeTest.csproj create mode 100644 OmniLinkBridgeTest/Properties/AssemblyInfo.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6895695 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +bin/ +obj/ +packages/ +.vs diff --git a/HAILogger.sln b/HAILogger.sln deleted file mode 100644 index 62ae7fd..0000000 --- a/HAILogger.sln +++ /dev/null @@ -1,22 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25123.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HAILogger", "HAILogger\HAILogger.csproj", "{0A636707-98BA-45AB-9843-AED430933CEE}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|x86 = Debug|x86 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {0A636707-98BA-45AB-9843-AED430933CEE}.Debug|x86.ActiveCfg = Debug|x86 - {0A636707-98BA-45AB-9843-AED430933CEE}.Debug|x86.Build.0 = Debug|x86 - {0A636707-98BA-45AB-9843-AED430933CEE}.Release|x86.ActiveCfg = Release|x86 - {0A636707-98BA-45AB-9843-AED430933CEE}.Release|x86.Build.0 = Release|x86 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/HAILogger/App.config b/HAILogger/App.config deleted file mode 100644 index 966d52a..0000000 --- a/HAILogger/App.config +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - diff --git a/HAILogger/CoreServer.cs b/HAILogger/CoreServer.cs deleted file mode 100644 index ab7ff38..0000000 --- a/HAILogger/CoreServer.cs +++ /dev/null @@ -1,1245 +0,0 @@ -using HAI_Shared; -using System; -using System.Collections.Generic; -using System.Data; -using System.Data.Odbc; -using System.Reflection; -using System.Text; -using System.Threading; - -namespace HAILogger -{ - class CoreServer - { - private bool quitting = false; - private bool terminate = false; - - // HAI Controller - private clsHAC HAC = null; - private DateTime retry = DateTime.MinValue; - private List alarms = new List(); - - // Thermostats - private Dictionary tstats = new Dictionary(); - private System.Timers.Timer tstat_timer = new System.Timers.Timer(); - private object tstat_lock = new object(); - - // Time Sync - private System.Timers.Timer tsync_timer = new System.Timers.Timer(); - private DateTime tsync_check = DateTime.MinValue; - - // 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(); - - public CoreServer() - { - Thread handler = new Thread(Server); - handler.Start(); - } - - private void Server() - { - Event.WriteInfo("CoreServer", "Starting up server " + - Assembly.GetExecutingAssembly().GetName().Version.ToString()); - - tstat_timer.Elapsed += tstat_timer_Elapsed; - tstat_timer.AutoReset = false; - - tsync_timer.Elapsed += tsync_timer_Elapsed; - tsync_timer.AutoReset = false; - - if (Global.mysql_logging) - { - Event.WriteInfo("DatabaseLogger", "Connecting to database"); - - mysql_conn = new OdbcConnection(Global.mysql_connection); - - // Must make an initial connection - if (!DBOpen()) - Environment.Exit(1); - } - - HAC = new clsHAC(); - - WebService web = new WebService(HAC); - - if (Global.webapi_enabled) - web.Start(); - - Connect(); - - while (true) - { - // End gracefully when not logging or database queue empty - if (quitting && (!Global.mysql_logging || DBQueueCount() == 0)) - break; - - // Make sure controller connection is active - if (HAC.Connection.ConnectionState == enuOmniLinkConnectionState.Offline && - retry < DateTime.Now) - { - Connect(); - } - - // Make sure database connection is active - if (Global.mysql_logging && mysql_conn.State != ConnectionState.Open) - { - // Nothing we can do if shutting down - if (quitting) - break; - - if (mysql_retry < DateTime.Now) - DBOpen(); - - if (mysql_conn.State != ConnectionState.Open) - { - // Loop to prevent database queries from executing - Thread.Sleep(1000); - continue; - } - } - - // Sleep when not logging or database queue empty - if (!Global.mysql_logging || DBQueueCount() == 0) - { - Thread.Sleep(1000); - continue; - } - - // Grab a copy in case the database query fails - string query; - lock (mysql_lock) - query = mysql_queue.Peek(); - - try - { - // Execute the database query - mysql_command = new OdbcCommand(query, mysql_conn); - mysql_command.ExecuteNonQuery(); - - // Successful remove query from queue - lock (mysql_lock) - mysql_queue.Dequeue(); - } - catch (Exception ex) - { - if (mysql_conn.State != ConnectionState.Open) - { - Event.WriteWarn("DatabaseLogger", "Lost connection to database"); - } - else - { - Event.WriteError("DatabaseLogger", "Error executing query\r\n" + ex.Message + "\r\n" + query); - - // Prevent an endless loop from failed query - lock (mysql_lock) - mysql_queue.Dequeue(); - } - } - } - - Event.WriteInfo("CoreServer", "Shutting down server"); - - if (Global.webapi_enabled) - web.Stop(); - - Disconnect(); - HAC = null; - - if (Global.mysql_logging) - DBClose(); - - terminate = true; - } - - private void Connected() - { - retry = DateTime.MinValue; - - GetNamedProperties(); - UnsolicitedNotifications(true); - - tstat_timer.Interval = ThermostatTimerInterval(); - tstat_timer.Start(); - - if (Global.hai_time_sync) - { - tsync_check = DateTime.MinValue; - - tsync_timer.Interval = TimeTimerInterval(); - tsync_timer.Start(); - } - } - - public void Shutdown() - { - quitting = true; - - while (!terminate) - Thread.Sleep(100); - - Event.WriteInfo("CoreServer", "Shutdown completed"); - } - - #region Connection - private void Connect() - { - if (HAC.Connection.ConnectionState == enuOmniLinkConnectionState.Offline) - { - retry = DateTime.Now.AddMinutes(1); - - HAC.Connection.NetworkAddress = Global.hai_address; - HAC.Connection.NetworkPort = (ushort)Global.hai_port; - HAC.Connection.ControllerKey = clsUtil.HexString2ByteArray( - String.Concat(Global.hai_key1, Global.hai_key2)); - - HAC.PreferredNetworkProtocol = clsHAC.enuPreferredNetworkProtocol.TCP; - HAC.Connection.ConnectionType = enuOmniLinkConnectionType.Network_TCP; - - HAC.Connection.Connect(HandleConnectStatus, HandleUnsolicitedPackets); - } - } - - private void Disconnect() - { - if (HAC.Connection.ConnectionState != enuOmniLinkConnectionState.Offline) - HAC.Connection.Disconnect(); - } - - private void HandleConnectStatus(enuOmniLinkCommStatus CS) - { - switch (CS) - { - case enuOmniLinkCommStatus.NoReply: - Event.WriteError("CoreServer", "CONNECTION STATUS: No Reply"); - break; - case enuOmniLinkCommStatus.UnrecognizedReply: - Event.WriteError("CoreServer", "CONNECTION STATUS: Unrecognized Reply"); - break; - case enuOmniLinkCommStatus.UnsupportedProtocol: - Event.WriteError("CoreServer", "CONNECTION STATUS: Unsupported Protocol"); - break; - case enuOmniLinkCommStatus.ClientSessionTerminated: - Event.WriteError("CoreServer", "CONNECTION STATUS: Client Session Terminated"); - break; - case enuOmniLinkCommStatus.ControllerSessionTerminated: - Event.WriteError("CoreServer", "CONNECTION STATUS: Controller Session Terminated"); - break; - case enuOmniLinkCommStatus.CannotStartNewSession: - Event.WriteError("CoreServer", "CONNECTION STATUS: Cannot Start New Session"); - break; - case enuOmniLinkCommStatus.LoginFailed: - Event.WriteError("CoreServer", "CONNECTION STATUS: Login Failed"); - break; - case enuOmniLinkCommStatus.UnableToOpenSocket: - Event.WriteError("CoreServer", "CONNECTION STATUS: Unable To Open Socket"); - break; - case enuOmniLinkCommStatus.UnableToConnect: - Event.WriteError("CoreServer", "CONNECTION STATUS: Unable To Connect"); - break; - case enuOmniLinkCommStatus.SocketClosed: - Event.WriteError("CoreServer", "CONNECTION STATUS: Socket Closed"); - break; - case enuOmniLinkCommStatus.UnexpectedError: - Event.WriteError("CoreServer", "CONNECTION STATUS: Unexpected Error"); - break; - case enuOmniLinkCommStatus.UnableToCreateSocket: - Event.WriteError("CoreServer", "CONNECTION STATUS: Unable To Create Socket"); - break; - case enuOmniLinkCommStatus.Retrying: - Event.WriteWarn("CoreServer", "CONNECTION STATUS: Retrying"); - break; - case enuOmniLinkCommStatus.Connected: - IdentifyController(); - break; - case enuOmniLinkCommStatus.Connecting: - Event.WriteInfo("CoreServer", "CONNECTION STATUS: Connecting"); - break; - case enuOmniLinkCommStatus.Disconnected: - Event.WriteInfo("CoreServer", "CONNECTION STATUS: Disconnected"); - break; - case enuOmniLinkCommStatus.InterruptedFunctionCall: - if (!quitting) - Event.WriteError("CoreServer", "CONNECTION STATUS: Interrupted Function Call"); - break; - case enuOmniLinkCommStatus.PermissionDenied: - Event.WriteError("CoreServer", "CONNECTION STATUS: Permission Denied"); - break; - case enuOmniLinkCommStatus.BadAddress: - Event.WriteError("CoreServer", "CONNECTION STATUS: Bad Address"); - break; - case enuOmniLinkCommStatus.InvalidArgument: - Event.WriteError("CoreServer", "CONNECTION STATUS: Invalid Argument"); - break; - case enuOmniLinkCommStatus.TooManyOpenFiles: - Event.WriteError("CoreServer", "CONNECTION STATUS: Too Many Open Files"); - break; - case enuOmniLinkCommStatus.ResourceTemporarilyUnavailable: - Event.WriteError("CoreServer", "CONNECTION STATUS: Resource Temporarily Unavailable"); - break; - case enuOmniLinkCommStatus.OperationNowInProgress: - Event.WriteWarn("CoreServer", "CONNECTION STATUS: Operation Now In Progress"); - break; - case enuOmniLinkCommStatus.OperationAlreadyInProgress: - Event.WriteWarn("CoreServer", "CONNECTION STATUS: Operation Already In Progress"); - break; - case enuOmniLinkCommStatus.SocketOperationOnNonSocket: - Event.WriteError("CoreServer", "CONNECTION STATUS: Socket Operation On Non Socket"); - break; - case enuOmniLinkCommStatus.DestinationAddressRequired: - Event.WriteError("CoreServer", "CONNECTION STATUS: Destination Address Required"); - break; - case enuOmniLinkCommStatus.MessgeTooLong: - Event.WriteError("CoreServer", "CONNECTION STATUS: Message Too Long"); - break; - case enuOmniLinkCommStatus.WrongProtocolType: - Event.WriteError("CoreServer", "CONNECTION STATUS: Wrong Protocol Type"); - break; - case enuOmniLinkCommStatus.BadProtocolOption: - Event.WriteError("CoreServer", "CONNECTION STATUS: Bad Protocol Option"); - break; - case enuOmniLinkCommStatus.ProtocolNotSupported: - Event.WriteError("CoreServer", "CONNECTION STATUS: Protocol Not Supported"); - break; - case enuOmniLinkCommStatus.SocketTypeNotSupported: - Event.WriteError("CoreServer", "CONNECTION STATUS: Socket Type Not Supported"); - break; - case enuOmniLinkCommStatus.OperationNotSupported: - Event.WriteError("CoreServer", "CONNECTION STATUS: Operation Not Supported"); - break; - case enuOmniLinkCommStatus.ProtocolFamilyNotSupported: - Event.WriteError("CoreServer", "CONNECTION STATUS: Protocol Family Not Supported"); - break; - case enuOmniLinkCommStatus.AddressFamilyNotSupported: - Event.WriteError("CoreServer", "CONNECTION STATUS: Address Family Not Supported"); - break; - case enuOmniLinkCommStatus.AddressInUse: - Event.WriteError("CoreServer", "CONNECTION STATUS: Address In Use"); - break; - case enuOmniLinkCommStatus.AddressNotAvailable: - Event.WriteError("CoreServer", "CONNECTION STATUS: Address Not Available"); - break; - case enuOmniLinkCommStatus.NetworkIsDown: - Event.WriteError("CoreServer", "CONNECTION STATUS: Network Is Down"); - break; - case enuOmniLinkCommStatus.NetworkIsUnreachable: - Event.WriteError("CoreServer", "CONNECTION STATUS: Network Is Unreachable"); - break; - case enuOmniLinkCommStatus.NetworkReset: - Event.WriteError("CoreServer", "CONNECTION STATUS: Network Reset"); - break; - case enuOmniLinkCommStatus.ConnectionAborted: - Event.WriteError("CoreServer", "CONNECTION STATUS: Connection Aborted"); - break; - case enuOmniLinkCommStatus.ConnectionResetByPeer: - Event.WriteError("CoreServer", "CONNECTION STATUS: Connection Reset By Peer"); - break; - case enuOmniLinkCommStatus.NoBufferSpaceAvailable: - Event.WriteError("CoreServer", "CONNECTION STATUS: No Buffer Space Available"); - break; - case enuOmniLinkCommStatus.AlreadyConnected: - Event.WriteWarn("CoreServer", "CONNECTION STATUS: Already Connected"); - break; - case enuOmniLinkCommStatus.NotConnected: - Event.WriteError("CoreServer", "CONNECTION STATUS: Not Connected"); - break; - case enuOmniLinkCommStatus.CannotSendAfterShutdown: - Event.WriteError("CoreServer", "CONNECTION STATUS: Cannot Send After Shutdown"); - break; - case enuOmniLinkCommStatus.ConnectionTimedOut: - Event.WriteError("CoreServer", "CONNECTION STATUS: Connection Timed Out"); - break; - case enuOmniLinkCommStatus.ConnectionRefused: - Event.WriteError("CoreServer", "CONNECTION STATUS: Connection Refused"); - break; - case enuOmniLinkCommStatus.HostIsDown: - Event.WriteError("CoreServer", "CONNECTION STATUS: Host Is Down"); - break; - case enuOmniLinkCommStatus.HostUnreachable: - Event.WriteError("CoreServer", "CONNECTION STATUS: Host Unreachable"); - break; - case enuOmniLinkCommStatus.TooManyProcesses: - Event.WriteError("CoreServer", "CONNECTION STATUS: Too Many Processes"); - break; - case enuOmniLinkCommStatus.NetworkSubsystemIsUnavailable: - Event.WriteError("CoreServer", "CONNECTION STATUS: Network Subsystem Is Unavailable"); - break; - case enuOmniLinkCommStatus.UnsupportedVersion: - Event.WriteError("CoreServer", "CONNECTION STATUS: Unsupported Version"); - break; - case enuOmniLinkCommStatus.NotInitialized: - Event.WriteError("CoreServer", "CONNECTION STATUS: Not Initialized"); - break; - case enuOmniLinkCommStatus.ShutdownInProgress: - Event.WriteError("CoreServer", "CONNECTION STATUS: Shutdown In Progress"); - break; - case enuOmniLinkCommStatus.ClassTypeNotFound: - Event.WriteError("CoreServer", "CONNECTION STATUS: Class Type Not Found"); - break; - case enuOmniLinkCommStatus.HostNotFound: - Event.WriteError("CoreServer", "CONNECTION STATUS: Host Not Found"); - break; - case enuOmniLinkCommStatus.HostNotFoundTryAgain: - Event.WriteError("CoreServer", "CONNECTION STATUS: Host Not Found Try Again"); - break; - case enuOmniLinkCommStatus.NonRecoverableError: - Event.WriteError("CoreServer", "CONNECTION STATUS: Non Recoverable Error"); - break; - case enuOmniLinkCommStatus.NoDataOfRequestedType: - Event.WriteError("CoreServer", "CONNECTION STATUS: No Data Of Requested Type"); - break; - default: - break; - } - } - - private void IdentifyController() - { - if (HAC.Connection.ConnectionState == enuOmniLinkConnectionState.Online || - HAC.Connection.ConnectionState == enuOmniLinkConnectionState.OnlineSecure) - { - HAC.Connection.Send(new clsOL2MsgRequestSystemInformation(HAC.Connection), HandleIdentifyController); - } - } - - private void HandleIdentifyController(clsOmniLinkMessageQueueItem M, byte[] B, bool Timeout) - { - if (Timeout) - return; - - if ((B.Length > 3) && (B[2] == (byte)enuOmniLink2MessageType.SystemInformation)) - { - clsOL2MsgSystemInformation MSG = new clsOL2MsgSystemInformation(HAC.Connection, B); - if (HAC.Model == MSG.ModelNumber) - { - HAC.CopySystemInformation(MSG); - Event.WriteInfo("CoreServer", "CONTROLLER IS: " + HAC.GetModelText() + " (" + HAC.GetVersionText() + ")"); - - Connected(); - return; - } - - Event.WriteError("CoreServer", "Model does not match file"); - HAC.Connection.Disconnect(); - } - } - #endregion - - #region Names - private void GetNamedProperties() - { - Event.WriteInfo("CoreServer", "Retrieving named units"); - - GetNextNamed(enuObjectType.Area, 0); - Thread.Sleep(100); - GetNextNamed(enuObjectType.Zone, 0); - Thread.Sleep(100); - GetNextNamed(enuObjectType.Thermostat, 0); - Thread.Sleep(100); - GetNextNamed(enuObjectType.Unit, 0); - Thread.Sleep(100); - GetNextNamed(enuObjectType.Message, 0); - Thread.Sleep(100); - GetNextNamed(enuObjectType.Button, 0); - Thread.Sleep(100); - } - - private void GetNextNamed(enuObjectType type, int ix) - { - clsOL2MsgRequestProperties MSG = new clsOL2MsgRequestProperties(HAC.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 - HAC.Connection.Send(MSG, HandleNamedPropertiesResponse); - } - - private void HandleNamedPropertiesResponse(clsOmniLinkMessageQueueItem M, byte[] B, bool Timeout) - { - if (Timeout) - return; - - // does it look like a valid response - if ((B.Length > 3) && (B[0] == 0x21)) - { - switch ((enuOmniLink2MessageType)B[2]) - { - case enuOmniLink2MessageType.EOD: - - break; - case enuOmniLink2MessageType.Properties: - - clsOL2MsgProperties MSG = new clsOL2MsgProperties(HAC.Connection, B); - - switch (MSG.ObjectType) - { - case enuObjectType.Area: - HAC.Areas.CopyProperties(MSG); - break; - case enuObjectType.Zone: - HAC.Zones.CopyProperties(MSG); - - if (HAC.Zones[MSG.ObjectNumber].IsTemperatureZone()) - HAC.Connection.Send(new clsOL2MsgRequestExtendedStatus(HAC.Connection, enuObjectType.Auxillary, MSG.ObjectNumber, MSG.ObjectNumber), HandleRequestAuxillaryStatus); - - break; - case enuObjectType.Thermostat: - HAC.Thermostats.CopyProperties(MSG); - - if (!tstats.ContainsKey(MSG.ObjectNumber)) - tstats.Add(MSG.ObjectNumber, DateTime.MinValue); - else - tstats[MSG.ObjectNumber] = DateTime.MinValue; - - HAC.Connection.Send(new clsOL2MsgRequestExtendedStatus(HAC.Connection, enuObjectType.Thermostat, MSG.ObjectNumber, MSG.ObjectNumber), HandleRequestThermostatStatus); - Event.WriteVerbose("ThermostatTimer", "Added to watch list " + HAC.Thermostats[MSG.ObjectNumber].Name); - break; - case enuObjectType.Unit: - HAC.Units.CopyProperties(MSG); - break; - case enuObjectType.Message: - HAC.Messages.CopyProperties(MSG); - break; - case enuObjectType.Button: - HAC.Buttons.CopyProperties(MSG); - break; - default: - break; - } - - GetNextNamed(MSG.ObjectType, MSG.ObjectNumber); - break; - default: - break; - } - } - } - #endregion - - #region Notifications - private void UnsolicitedNotifications(bool enable) - { - Event.WriteInfo("CoreServer", "Unsolicited notifications " + (enable ? "enabled" : "disabled")); - HAC.Connection.Send(new clsOL2EnableNotifications(HAC.Connection, enable), null); - } - - private bool HandleUnsolicitedPackets(byte[] B) - { - if ((B.Length > 3) && (B[0] == 0x21)) - { - bool handled = false; - - switch ((enuOmniLink2MessageType)B[2]) - { - case enuOmniLink2MessageType.ClearNames: - break; - case enuOmniLink2MessageType.DownloadNames: - break; - case enuOmniLink2MessageType.UploadNames: - break; - case enuOmniLink2MessageType.NameData: - break; - case enuOmniLink2MessageType.ClearVoices: - break; - case enuOmniLink2MessageType.DownloadVoices: - break; - case enuOmniLink2MessageType.UploadVoices: - break; - case enuOmniLink2MessageType.VoiceData: - break; - case enuOmniLink2MessageType.Command: - break; - case enuOmniLink2MessageType.EnableNotifications: - break; - case enuOmniLink2MessageType.SystemInformation: - break; - case enuOmniLink2MessageType.SystemStatus: - break; - case enuOmniLink2MessageType.SystemTroubles: - break; - case enuOmniLink2MessageType.SystemFeatures: - break; - case enuOmniLink2MessageType.Capacities: - break; - case enuOmniLink2MessageType.Properties: - break; - case enuOmniLink2MessageType.Status: - break; - case enuOmniLink2MessageType.EventLogItem: - break; - case enuOmniLink2MessageType.ValidateCode: - break; - case enuOmniLink2MessageType.SystemFormats: - break; - case enuOmniLink2MessageType.Login: - break; - case enuOmniLink2MessageType.Logout: - break; - case enuOmniLink2MessageType.ActivateKeypadEmg: - break; - case enuOmniLink2MessageType.ExtSecurityStatus: - break; - case enuOmniLink2MessageType.CmdExtSecurity: - break; - case enuOmniLink2MessageType.AudioSourceStatus: - break; - case enuOmniLink2MessageType.SystemEvents: - HandleUnsolicitedSystemEvent(B); - handled = true; - break; - case enuOmniLink2MessageType.ZoneReadyStatus: - break; - case enuOmniLink2MessageType.ExtendedStatus: - HandleUnsolicitedExtendedStatus(B); - handled = true; - break; - default: - break; - } - - if (Global.verbose_unhandled && !handled) - Event.WriteVerbose("CoreServer", "Unhandled notification: " + ((enuOmniLink2MessageType)B[2]).ToString()); - } - - return true; - } - - private void HandleUnsolicitedSystemEvent(byte[] B) - { - clsOL2SystemEvent MSG = new clsOL2SystemEvent(HAC.Connection, B); - - enuEventType type; - string value = string.Empty; - bool alert = false; - - if (MSG.SystemEvent >= 1 && MSG.SystemEvent <= 255) - { - type = enuEventType.USER_MACRO_BUTTON; - value = ((int)MSG.SystemEvent).ToString() + " " + HAC.Buttons[MSG.SystemEvent].Name; - - LogEventStatus(type, value, alert); - } - else if (MSG.SystemEvent >= 768 && MSG.SystemEvent <= 771) - { - type = enuEventType.PHONE_; - - if (MSG.SystemEvent == 768) - { - value = "DEAD"; - alert = true; - } - else if (MSG.SystemEvent == 769) - value = "RING"; - else if (MSG.SystemEvent == 770) - value = "OFF HOOK"; - else if (MSG.SystemEvent == 771) - value = "ON HOOK"; - - LogEventStatus(type, value, alert); - } - else if (MSG.SystemEvent >= 772 && MSG.SystemEvent <= 773) - { - type = enuEventType.AC_POWER_; - alert = true; - - if (MSG.SystemEvent == 772) - value = "OFF"; - else if (MSG.SystemEvent == 773) - value = "RESTORED"; - - LogEventStatus(type, value, alert); - } - else if (MSG.SystemEvent >= 774 && MSG.SystemEvent <= 775) - { - type = enuEventType.BATTERY_; - alert = true; - - if (MSG.SystemEvent == 774) - value = "LOW"; - else if (MSG.SystemEvent == 775) - value = "OK"; - - LogEventStatus(type, value, alert); - } - else if (MSG.SystemEvent >= 776 && MSG.SystemEvent <= 777) - { - type = enuEventType.DCM_; - alert = true; - - if (MSG.SystemEvent == 776) - value = "TROUBLE"; - else if (MSG.SystemEvent == 777) - value = "OK"; - - LogEventStatus(type, value, alert); - } - else if (MSG.SystemEvent >= 778 && MSG.SystemEvent <= 781) - { - type = enuEventType.ENERGY_COST_; - - if (MSG.SystemEvent == 778) - value = "LOW"; - else if (MSG.SystemEvent == 779) - value = "MID"; - else if (MSG.SystemEvent == 780) - value = "HIGH"; - else if (MSG.SystemEvent == 781) - value = "CRITICAL"; - - LogEventStatus(type, value, alert); - } - else if (MSG.SystemEvent >= 782 && MSG.SystemEvent <= 787) - { - type = enuEventType.CAMERA; - value = (MSG.SystemEvent - 781).ToString(); - - LogEventStatus(type, value, alert); - } - else if (MSG.SystemEvent >= 61440 && MSG.SystemEvent <= 64511) - { - type = enuEventType.SWITCH_PRESS; - int state = (int)MSG.Data[1] - 240; - int id = (int)MSG.Data[2]; - - LogEventStatus(type, "Unit: " + id + ", State: " + state, alert); - } - else if (MSG.SystemEvent >= 64512 && MSG.SystemEvent <= 65535) - { - type = enuEventType.UPB_LINK; - int state = (int)MSG.Data[1] - 252; - int id = (int)MSG.Data[2]; - - LogEventStatus(type, "Link: " + id + ", State: " + state, alert); - } - else if (Global.verbose_unhandled) - { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < MSG.MessageLength; i++) - sb.Append(MSG.Data[i].ToString() + " "); - Event.WriteVerbose("SystemEvent", "Unhandled Raw: " + sb.ToString() + "Num: " + MSG.SystemEvent); - - int num = ((int)MSG.MessageLength - 1) / 2; - for (int i = 0; i < num; i++) - { - Event.WriteVerbose("SystemEvent", "Unhandled: " + - (int)MSG.Data[(i * 2) + 1] + " " + (int)MSG.Data[(i * 2) + 2] + ": " + - Convert.ToString(MSG.Data[(i * 2) + 1], 2).PadLeft(8, '0') + " " + Convert.ToString(MSG.Data[(i * 2) + 2], 2).PadLeft(8, '0')); - } - } - } - - private void HandleUnsolicitedExtendedStatus(byte[] B) - { - clsOL2MsgExtendedStatus MSG = new clsOL2MsgExtendedStatus(HAC.Connection, B); - - switch (MSG.ObjectType) - { - case enuObjectType.Area: - for (byte i = 0; i < MSG.AreaCount(); i++) - { - HAC.Areas[MSG.ObjectNumber(i)].CopyExtendedStatus(MSG, i); - LogAreaStatus(MSG.ObjectNumber(i)); - - WebNotification.Send("area", Helper.Serialize(Helper.ConvertArea( - MSG.ObjectNumber(i), HAC.Areas[MSG.ObjectNumber(i)]))); - } - break; - case enuObjectType.Auxillary: - for (byte i = 0; i < MSG.AuxStatusCount(); i++) - { - HAC.Zones[MSG.ObjectNumber(i)].CopyAuxExtendedStatus(MSG, i); - LogZoneStatus(MSG.ObjectNumber(i)); - - if (HAC.Zones[MSG.ObjectNumber(i)].IsTemperatureZone()) - { - WebNotification.Send("temp", Helper.Serialize(Helper.ConvertZone( - MSG.ObjectNumber(i), HAC.Zones[MSG.ObjectNumber(i)]))); - } - } - break; - case enuObjectType.Zone: - for (byte i = 0; i < MSG.ZoneStatusCount(); i++) - { - HAC.Zones[MSG.ObjectNumber(i)].CopyExtendedStatus(MSG, i); - LogZoneStatus(MSG.ObjectNumber(i)); - - switch (HAC.Zones[MSG.ObjectNumber(i)].ZoneType) - { - case enuZoneType.EntryExit: - case enuZoneType.X2EntryDelay: - case enuZoneType.X4EntryDelay: - case enuZoneType.Perimeter: - case enuZoneType.Tamper: - case enuZoneType.Auxiliary: - WebNotification.Send("contact", Helper.Serialize(Helper.ConvertZone( - MSG.ObjectNumber(i), HAC.Zones[MSG.ObjectNumber(i)]))); - break; - case enuZoneType.AwayInt: - case enuZoneType.NightInt: - WebNotification.Send("motion", Helper.Serialize(Helper.ConvertZone( - MSG.ObjectNumber(i), HAC.Zones[MSG.ObjectNumber(i)]))); - break; - case enuZoneType.Water: - WebNotification.Send("water", Helper.Serialize(Helper.ConvertZone( - MSG.ObjectNumber(i), HAC.Zones[MSG.ObjectNumber(i)]))); - break; - case enuZoneType.Fire: - WebNotification.Send("smoke", Helper.Serialize(Helper.ConvertZone( - MSG.ObjectNumber(i), HAC.Zones[MSG.ObjectNumber(i)]))); - break; - case enuZoneType.Gas: - WebNotification.Send("co", Helper.Serialize(Helper.ConvertZone( - MSG.ObjectNumber(i), HAC.Zones[MSG.ObjectNumber(i)]))); - break; - } - } - break; - case enuObjectType.Thermostat: - for (byte i = 0; i < MSG.ThermostatStatusCount(); i++) - { - lock (tstat_lock) - { - HAC.Thermostats[MSG.ObjectNumber(i)].CopyExtendedStatus(MSG, i); - - if (!tstats.ContainsKey(MSG.ObjectNumber(i))) - tstats.Add(MSG.ObjectNumber(i), DateTime.Now); - else - tstats[MSG.ObjectNumber(i)] = DateTime.Now; - - if (Global.verbose_thermostat_timer) - Event.WriteVerbose("ThermostatTimer", "Unsolicited status received for " + HAC.Thermostats[MSG.ObjectNumber(i)].Name); - - WebNotification.Send("thermostat", Helper.Serialize(Helper.ConvertThermostat( - MSG.ObjectNumber(i), HAC.Thermostats[MSG.ObjectNumber(i)]))); - } - } - break; - case enuObjectType.Unit: - for (byte i = 0; i < MSG.UnitStatusCount(); i++) - { - HAC.Units[MSG.ObjectNumber(i)].CopyExtendedStatus(MSG, i); - LogUnitStatus(MSG.ObjectNumber(i)); - - WebNotification.Send("unit", Helper.Serialize(Helper.ConvertUnit( - MSG.ObjectNumber(i), HAC.Units[MSG.ObjectNumber(i)]))); - } - break; - case enuObjectType.Message: - for (byte i = 0; i < MSG.MessageCount(); i++) - { - HAC.Messages[MSG.ObjectNumber(i)].CopyExtendedStatus(MSG, i); - LogMessageStatus(MSG.ObjectNumber(i)); - } - break; - default: - if (Global.verbose_unhandled) - { - StringBuilder sb = new StringBuilder(); - foreach (byte b in MSG.ToByteArray()) - sb.Append(b.ToString() + " "); - Event.WriteVerbose("ExtendedStatus", "Unhandled " + MSG.ObjectType.ToString() + " " + sb.ToString()); - } - break; - } - } - #endregion - - private void HandleRequestAuxillaryStatus(clsOmniLinkMessageQueueItem M, byte[] B, bool Timeout) - { - if (Timeout) - return; - - clsOL2MsgExtendedStatus MSG = new clsOL2MsgExtendedStatus(HAC.Connection, B); - - for (byte i = 0; i < MSG.AuxStatusCount(); i++) - { - HAC.Zones[MSG.ObjectNumber(i)].CopyAuxExtendedStatus(MSG, i); - } - } - - #region Thermostats - static double ThermostatTimerInterval() - { - DateTime now = DateTime.Now; - return ((60 - now.Second) * 1000 - now.Millisecond) + new TimeSpan(0, 0, 30).TotalMilliseconds; - } - - private static DateTime RoundToMinute(DateTime dt) - { - return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, 0); - } - - private void tstat_timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) - { - lock (tstat_lock) - { - foreach (KeyValuePair tstat in tstats) - { - // Poll every 4 minutes if no prior update - if (RoundToMinute(tstat.Value).AddMinutes(4) <= RoundToMinute(DateTime.Now)) - { - HAC.Connection.Send(new clsOL2MsgRequestExtendedStatus(HAC.Connection, enuObjectType.Thermostat, tstat.Key, tstat.Key), HandleRequestThermostatStatus); - - if (Global.verbose_thermostat_timer) - Event.WriteVerbose("ThermostatTimer", "Polling status for " + HAC.Thermostats[tstat.Key].Name); - } - - // Log every minute if update within 5 minutes and connected - if (RoundToMinute(tstat.Value).AddMinutes(5) > RoundToMinute(DateTime.Now) && - (HAC.Connection.ConnectionState == enuOmniLinkConnectionState.Online || - HAC.Connection.ConnectionState == enuOmniLinkConnectionState.OnlineSecure)) - { - if (HAC.Thermostats[tstat.Key].Temp > 0) - LogThermostatStatus(tstat.Key); - else if (Global.verbose_thermostat_timer) - Event.WriteWarn("ThermostatTimer", "Not logging unknown temp for " + HAC.Thermostats[tstat.Key].Name); - } - else if (Global.verbose_thermostat_timer) - Event.WriteWarn("ThermostatTimer", "Not logging out of date status for " + HAC.Thermostats[tstat.Key].Name); - } - } - - tstat_timer.Interval = ThermostatTimerInterval(); - tstat_timer.Start(); - } - - private void HandleRequestThermostatStatus(clsOmniLinkMessageQueueItem M, byte[] B, bool Timeout) - { - if (Timeout) - return; - - clsOL2MsgExtendedStatus MSG = new clsOL2MsgExtendedStatus(HAC.Connection, B); - - for (byte i = 0; i < MSG.ThermostatStatusCount(); i++) - { - lock (tstat_lock) - { - HAC.Thermostats[MSG.ObjectNumber(i)].CopyExtendedStatus(MSG, i); - - if (!tstats.ContainsKey(MSG.ObjectNumber(i))) - tstats.Add(MSG.ObjectNumber(i), DateTime.Now); - else - tstats[MSG.ObjectNumber(i)] = DateTime.Now; - - if (Global.verbose_thermostat_timer) - Event.WriteVerbose("ThermostatTimer", "Polling status received for " + HAC.Thermostats[MSG.ObjectNumber(i)].Name); - } - } - } - #endregion - - #region Time Sync - static double TimeTimerInterval() - { - DateTime now = DateTime.Now; - return ((60 - now.Second) * 1000 - now.Millisecond); - } - - private void tsync_timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) - { - if (tsync_check.AddMinutes(Global.hai_time_interval) < DateTime.Now) - HAC.Connection.Send(new clsOL2MsgRequestSystemStatus(HAC.Connection), HandleRequestSystemStatus); - - tsync_timer.Interval = TimeTimerInterval(); - tsync_timer.Start(); - } - - private void HandleRequestSystemStatus(clsOmniLinkMessageQueueItem M, byte[] B, bool Timeout) - { - if (Timeout) - return; - - tsync_check = DateTime.Now; - - clsOL2MsgSystemStatus MSG = new clsOL2MsgSystemStatus(HAC.Connection, B); - - DateTime time; - try - { - // The controller uses 2 digit years and C# uses 4 digit years - // 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); - } - catch - { - Event.WriteWarn("TimeSyncTimer", "Controller time could not be parsed", true); - - DateTime now = DateTime.Now; - HAC.Connection.Send(new clsOL2MsgSetTime(HAC.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; - } - - double adj = (DateTime.Now - time).Duration().TotalSeconds; - - if (adj > Global.hai_time_drift) - { - Event.WriteWarn("TimeSyncTimer", "Controller time " + time.ToString("MM/dd/yyyy HH:mm:ss") + " out of sync by " + adj + " seconds", true); - - DateTime now = DateTime.Now; - HAC.Connection.Send(new clsOL2MsgSetTime(HAC.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); - } - } - - private void HandleSetTime(clsOmniLinkMessageQueueItem M, byte[] B, bool Timeout) - { - if (Timeout) - return; - - Event.WriteVerbose("TimeSyncTimer", "Controller time has been successfully set"); - } - #endregion - - #region Logging - private void LogEventStatus(enuEventType type, string value, bool alert) - { - DBQueue(@" - INSERT INTO log_events (timestamp, name, status) - VALUES ('" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "','" + type.ToString() + "','" + value + "')"); - - if (alert) - { - Event.WriteWarn("SystemEvent", type.ToString() + " " + value); - Prowl.Notify("SystemEvent", type.ToString() + " " + value); - } - - if (Global.verbose_event) - Event.WriteVerbose("SystemEvent", type.ToString() + " " + value); - } - - private void LogAreaStatus(ushort id) - { - clsArea unit = HAC.Areas[id]; - - // Alarm notifcation - if (unit.AreaFireAlarmText != "OK") - { - Event.WriteAlarm("AreaStatus", "FIRE " + unit.Name + " " + unit.AreaFireAlarmText); - Prowl.Notify("ALARM", "FIRE " + unit.Name + " " + unit.AreaFireAlarmText, ProwlPriority.Emergency); - - if (!alarms.Contains("FIRE" + id)) - alarms.Add("FIRE" + id); - } - else if (alarms.Contains("FIRE" + id)) - { - Event.WriteAlarm("AreaStatus", "CLEARED - FIRE " + unit.Name + " " + unit.AreaFireAlarmText); - Prowl.Notify("ALARM CLEARED", "FIRE " + unit.Name + " " + unit.AreaFireAlarmText, ProwlPriority.High); - - alarms.Remove("FIRE" + id); - } - - if (unit.AreaBurglaryAlarmText != "OK") - { - Event.WriteAlarm("AreaStatus", "BURGLARY " + unit.Name + " " + unit.AreaBurglaryAlarmText); - Prowl.Notify("ALARM", "BURGLARY " + unit.Name + " " + unit.AreaBurglaryAlarmText, ProwlPriority.Emergency); - - if (!alarms.Contains("BURGLARY" + id)) - alarms.Add("BURGLARY" + id); - } - else if (alarms.Contains("BURGLARY" + id)) - { - Event.WriteAlarm("AreaStatus", "CLEARED - BURGLARY " + unit.Name + " " + unit.AreaBurglaryAlarmText); - Prowl.Notify("ALARM CLEARED", "BURGLARY " + unit.Name + " " + unit.AreaBurglaryAlarmText, ProwlPriority.High); - - alarms.Remove("BURGLARY" + id); - } - - if (unit.AreaAuxAlarmText != "OK") - { - Event.WriteAlarm("AreaStatus", "AUX " + unit.Name + " " + unit.AreaAuxAlarmText); - Prowl.Notify("ALARM", "AUX " + unit.Name + " " + unit.AreaAuxAlarmText, ProwlPriority.Emergency); - - if (!alarms.Contains("AUX" + id)) - alarms.Add("AUX" + id); - } - else if (alarms.Contains("AUX" + id)) - { - Event.WriteAlarm("AreaStatus", "CLEARED - AUX " + unit.Name + " " + unit.AreaAuxAlarmText); - Prowl.Notify("ALARM CLEARED", "AUX " + unit.Name + " " + unit.AreaAuxAlarmText, ProwlPriority.High); - - alarms.Remove("AUX" + id); - } - - if (unit.AreaDuressAlarmText != "OK") - { - Event.WriteAlarm("AreaStatus", "DURESS " + unit.Name + " " + unit.AreaDuressAlarmText); - Prowl.Notify("ALARM", "DURESS " + unit.Name + " " + unit.AreaDuressAlarmText, ProwlPriority.Emergency); - - if (!alarms.Contains("DURESS" + id)) - alarms.Add("DURESS" + id); - } - else if (alarms.Contains("DURESS" + id)) - { - Event.WriteAlarm("AreaStatus", "CLEARED - DURESS " + unit.Name + " " + unit.AreaDuressAlarmText); - Prowl.Notify("ALARM CLEARED", "DURESS " + unit.Name + " " + unit.AreaDuressAlarmText, ProwlPriority.High); - - alarms.Remove("DURESS" + id); - } - - string status = unit.ModeText(); - - if (unit.ExitTimer > 0) - status = "ARMING " + status; - - if (unit.EntryTimer > 0) - status = "TRIPPED " + status; - - DBQueue(@" - INSERT INTO log_areas (timestamp, id, name, - fire, police, auxiliary, - duress, security) - VALUES ('" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "','" + id.ToString() + "','" + unit.Name + "','" + - unit.AreaFireAlarmText + "','" + unit.AreaBurglaryAlarmText + "','" + unit.AreaAuxAlarmText + "','" + - unit.AreaDuressAlarmText + "','" + status + "')"); - - if (Global.verbose_area) - Event.WriteVerbose("AreaStatus", id + " " + unit.Name + ", Status: " + status); - - if (unit.LastMode != unit.AreaMode) - Prowl.Notify("Security", unit.Name + " " + unit.ModeText()); - } - - private void LogZoneStatus(ushort id) - { - clsZone unit = HAC.Zones[id]; - - DBQueue(@" - INSERT INTO log_zones (timestamp, id, name, status) - VALUES ('" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "','" + id.ToString() + "','" + unit.Name + "','" + unit.StatusText() + "')"); - - if (Global.verbose_zone) - { - if (unit.IsTemperatureZone()) - Event.WriteVerbose("ZoneStatus", id + " " + unit.Name + ", Temp: " + unit.TempText()); - else - Event.WriteVerbose("ZoneStatus", id + " " + unit.Name + ", Status: " + unit.StatusText()); - } - } - - private void LogThermostatStatus(ushort id) - { - clsThermostat unit = HAC.Thermostats[id]; - - int temp, heat, cool, humidity, humidify, dehumidify; - - Int32.TryParse(unit.TempText(), out temp); - Int32.TryParse(unit.HeatSetpointText(), out heat); - Int32.TryParse(unit.CoolSetpointText(), out cool); - Int32.TryParse(unit.HumidityText(), out humidity); - Int32.TryParse(unit.HumidifySetpointText(), out humidify); - Int32.TryParse(unit.DehumidifySetpointText(), out dehumidify); - - DBQueue(@" - INSERT INTO log_thermostats (timestamp, id, name, - status, temp, heat, cool, - humidity, humidify, dehumidify, - mode, fan, hold) - VALUES ('" + DateTime.Now.ToString("yyyy-MM-dd HH:mm") + "','" + id.ToString() + "','" + unit.Name + "','" + - unit.HorC_StatusText() + "','" + temp.ToString() + "','" + heat + "','" + cool + "','" + - humidity + "','" + humidify + "','" + dehumidify + "','" + - unit.ModeText() + "','" + unit.FanModeText() + "','" + unit.HoldStatusText() + "')"); - - if (Global.verbose_thermostat) - Event.WriteVerbose("ThermostatStatus", id + " " + unit.Name + - ", Status: " + unit.TempText() + " " + unit.HorC_StatusText() + - ", Heat: " + unit.HeatSetpointText() + - ", Cool: " + unit.CoolSetpointText() + - ", Mode: " + unit.ModeText() + - ", Fan: " + unit.FanModeText() + - ", Hold: " + unit.HoldStatusText()); - } - - private void LogUnitStatus(ushort id) - { - clsUnit unit = HAC.Units[id]; - - string status = unit.StatusText; - - if (unit.Status == 100 && unit.StatusTime == 0) - status = "OFF"; - else if (unit.Status == 200 && unit.StatusTime == 0) - status = "ON"; - - DBQueue(@" - INSERT INTO log_units (timestamp, id, name, - status, statusvalue, statustime) - VALUES ('" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "','" + id.ToString() + "','" + unit.Name + "','" + - status + "','" + unit.Status + "','" + unit.StatusTime + "')"); - - if (Global.verbose_unit) - Event.WriteVerbose("UnitStatus", id + " " + unit.Name + ", Status: " + status); - } - - private void LogMessageStatus(ushort id) - { - clsMessage unit = HAC.Messages[id]; - - DBQueue(@" - INSERT INTO log_messages (timestamp, id, name, status) - VALUES ('" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "','" + id.ToString() + "','" + unit.Name + "','" + unit.StatusText() + "')"); - - if (Global.verbose_message) - Event.WriteVerbose("MessageStatus", unit.Name + ", " + unit.StatusText()); - - if (Global.prowl_messages) - Prowl.Notify("Message", id + " " + unit.Name + ", " + unit.StatusText()); - } - - #endregion - - #region Database - public bool DBOpen() - { - try - { - if (mysql_conn.State != ConnectionState.Open) - mysql_conn.Open(); - - mysql_retry = DateTime.MinValue; - } - catch (Exception ex) - { - Event.WriteError("DatabaseLogger", "Failed to connect to database\r\n" + ex.Message); - mysql_retry = DateTime.Now.AddMinutes(1); - return false; - } - - return true; - } - - public void DBClose() - { - if (mysql_conn.State != ConnectionState.Closed) - mysql_conn.Close(); - } - - public void DBQueue(string query) - { - if (!Global.mysql_logging) - return; - - lock (mysql_lock) - mysql_queue.Enqueue(query); - } - - private int DBQueueCount() - { - int count; - lock (mysql_lock) - count = mysql_queue.Count; - - return count; - } - #endregion - } -} \ No newline at end of file diff --git a/HAILogger/Event.cs b/HAILogger/Event.cs deleted file mode 100644 index 2c4bb4a..0000000 --- a/HAILogger/Event.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Net; -using System.Net.Mail; - -namespace HAILogger -{ - static class Event - { - public static void WriteVerbose(string value) - { - Trace.WriteLine(value); - LogFile(TraceLevel.Verbose, "VERBOSE", value); - } - - public static void WriteVerbose(string source, string value) - { - Trace.WriteLine("VERBOSE: " + source + ": " + value); - LogFile(TraceLevel.Verbose, "VERBOSE: " + source, value); - } - - public static void WriteInfo(string source, string value, bool alert) - { - WriteInfo(source, value); - if (alert) - { - LogEvent(EventLogEntryType.Information, source, value); - SendMail("Info", source, value); - } - } - - public static void WriteInfo(string source, string value) - { - Trace.WriteLine("INFO: " + source + ": " + value); - LogFile(TraceLevel.Info, "INFO: " + source, value); - } - - public static void WriteWarn(string source, string value, bool alert) - { - WriteWarn(source, value); - if (alert) - SendMail("Warn", source, value); - } - - public static void WriteWarn(string source, string value) - { - Trace.WriteLine("WARN: " + source + ": " + value); - LogFile(TraceLevel.Warning, "WARN: " + source, value); - LogEvent(EventLogEntryType.Warning, source, value); - } - - public static void WriteError(string source, string value) - { - Trace.WriteLine("ERROR: " + source + ": " + value); - LogFile(TraceLevel.Error, "ERROR: " + source, value); - LogEvent(EventLogEntryType.Error, source, value); - SendMail("Error", source, value); - } - - public static void WriteAlarm(string source, string value) - { - Trace.WriteLine("ALARM: " + source + ": " + value); - LogFile(TraceLevel.Error, "ALARM: " + source, value); - LogEvent(EventLogEntryType.Warning, source, value); - - if (Global.mail_alarm_to != null && Global.mail_alarm_to.Length > 0) - { - MailMessage mail = new MailMessage(); - mail.From = Global.mail_from; - foreach (MailAddress address in Global.mail_alarm_to) - mail.To.Add(address); - mail.Priority = MailPriority.High; - mail.Subject = value; - mail.Body = value; - - SmtpClient smtp = new SmtpClient(Global.mail_server, Global.mail_port); - - if (!string.IsNullOrEmpty(Global.mail_username)) - { - smtp.UseDefaultCredentials = false; - smtp.Credentials = new NetworkCredential(Global.mail_username, Global.mail_password); - } - - try - { - smtp.Send(mail); - } - catch (Exception ex) - { - string error = "An error occurred sending email notification\r\n" + ex.Message; - LogFile(TraceLevel.Error, "ERROR: " + source, error); - LogEvent(EventLogEntryType.Error, "EventNotification", error); - } - } - } - - private static void SendMail(string level, string source, string value) - { - if (Global.mail_to == null || Global.mail_to.Length == 0) - return; - - MailMessage mail = new MailMessage(); - mail.From = Global.mail_from; - foreach (MailAddress address in Global.mail_to) - mail.To.Add(address); - mail.Subject = Global.event_source + " - " + level; - mail.Body = source + ": " + value; - - SmtpClient smtp = new SmtpClient(Global.mail_server, Global.mail_port); - - if (!string.IsNullOrEmpty(Global.mail_username)) - { - smtp.UseDefaultCredentials = false; - smtp.Credentials = new NetworkCredential(Global.mail_username, Global.mail_password); - } - - try - { - smtp.Send(mail); - } - catch (Exception ex) - { - string error = "An error occurred sending email notification\r\n" + ex.Message; - LogFile(TraceLevel.Error, "ERROR: " + source, error); - LogEvent(EventLogEntryType.Error, "EventNotification", error); - } - } - - private static void LogEvent(EventLogEntryType type, string source, string value) - { - string event_log = "Application"; - - if (!EventLog.SourceExists(Global.event_source)) - EventLog.CreateEventSource(Global.event_source, event_log); - - string event_msg = source + ": " + value; - - EventLog.WriteEntry(Global.event_source, event_msg, type, 234); - } - - private static void LogFile(TraceLevel level, string source, string value) - { - TraceSwitch tswitch = new TraceSwitch("TraceLevelSwitch", "Trace Level for Entire Application"); - - if (tswitch.Level < level) - return; - - try - { - FileStream fs = new FileStream(Global.log_file, FileMode.Append, FileAccess.Write); - StreamWriter sw = new StreamWriter(fs); - - sw.WriteLine(DateTime.Now.ToString("MM/dd/yyyy HH:mm:ss ") + source + ": " + value); - - sw.Close(); - fs.Close(); - } - catch - { - LogEvent(EventLogEntryType.Error, "EventLogger", "Unable to write to the file log."); - } - } - } -} diff --git a/HAILogger/Global.cs b/HAILogger/Global.cs deleted file mode 100644 index eb46923..0000000 --- a/HAILogger/Global.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Net.Mail; - -namespace HAILogger -{ - public abstract class Global - { - // Events - public static string event_source; - - // Files - public static string config_file; - public static string log_file; - - // HAI Controller - public static string hai_address; - public static int hai_port; - public static string hai_key1; - public static string hai_key2; - public static bool hai_time_sync; - public static int hai_time_interval; - public static int hai_time_drift; - - // mySQL Database - public static bool mysql_logging; - public static string mysql_connection; - - // Events - public static string mail_server; - public static int mail_port; - public static string mail_username; - public static string mail_password; - public static MailAddress mail_from; - public static MailAddress[] mail_to; - public static MailAddress[] mail_alarm_to; - - // Prowl Notifications - public static string[] prowl_key; - public static bool prowl_messages; - - // Web Service - public static bool webapi_enabled; - public static int webapi_port; - - // Verbose Output - public static bool verbose_unhandled; - public static bool verbose_event; - public static bool verbose_area; - public static bool verbose_zone; - public static bool verbose_thermostat_timer; - public static bool verbose_thermostat; - public static bool verbose_unit; - public static bool verbose_message; - } -} diff --git a/HAILogger/HAILogger.csproj b/HAILogger/HAILogger.csproj deleted file mode 100644 index 627e6c3..0000000 --- a/HAILogger/HAILogger.csproj +++ /dev/null @@ -1,107 +0,0 @@ - - - - Debug - x86 - 8.0.30703 - 2.0 - {0A636707-98BA-45AB-9843-AED430933CEE} - Exe - Properties - HAILogger - HAILogger - v4.0 - 512 - - - - x86 - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - x86 - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - .\HAI.Controller.dll - - - - - - - - - - - - - - - - - - - - - - Component - - - ProjectInstaller.cs - - - - - Component - - - Service.cs - - - - - - - - - - - - - - Designer - - - Always - - - - - ProjectInstaller.cs - - - Service.cs - - - - - \ No newline at end of file diff --git a/HAILogger/HAILogger.ini b/HAILogger/HAILogger.ini deleted file mode 100644 index 7677909..0000000 --- a/HAILogger/HAILogger.ini +++ /dev/null @@ -1,55 +0,0 @@ -# HAI Controller -hai_address = -hai_port = 4369 -hai_key1 = 00-00-00-00-00-00-00-00 -hai_key2 = 00-00-00-00-00-00-00-00 - -# HAI Controller Time Sync (yes/no) -# hai_time_check is interval in minutes to check controller time -# hai_time_adj is the drift in seconds to allow before an adjustment is made -hai_time_sync = yes -hai_time_interval = 60 -hai_time_drift = 10 - -# mySQL Database Logging (yes/no) -mysql_logging = no -mysql_server = -mysql_database = -mysql_user = -mysql_password = - -# Event Notifications -# mail_username and mail_password optional for authenticated mail -# mail_to sent for service notifications -# mail_alarm_to sent for FIRE, BURGLARY, AUX, DURESS -mail_server = -mail_port = 25 -mail_username = -mail_password = -mail_from = -#mail_to = -#mail_to = -#mail_alarm_to = -#mail_alarm_to = - -# Prowl Notifications -# Register for API key at http://www.prowlapp.com -# Sent for FIRE, BURGLARY, AUX, DURESS -# prowl_messages (yes/no) for console message notifications -#prowl_key = -#prowl_key = -prowl_messages = no - -# Web service -# Used for integration with Samsung SmartThings -webapi_enabled = yes -webapi_port = 8000 - -# Verbose Output (yes/no) -verbose_unhandled = yes -verbose_area = yes -verbose_zone = yes -verbose_thermostat_timer = yes -verbose_thermostat = yes -verbose_unit = yes -verbose_message = yes \ No newline at end of file diff --git a/HAILogger/Prowl.cs b/HAILogger/Prowl.cs deleted file mode 100644 index 8fb1b5e..0000000 --- a/HAILogger/Prowl.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; - -namespace HAILogger -{ - public enum ProwlPriority - { - VeryLow = -2, - Moderate = -1, - Normal = 0, - High = 1, - Emergency = 2, - }; - - static class Prowl - { - public static void Notify(string source, string description) - { - Notify(source, description, ProwlPriority.Normal); - } - - public static void Notify(string source, string description, ProwlPriority priority) - { - Uri URI = new Uri("https://api.prowlapp.com/publicapi/add"); - - foreach (string key in Global.prowl_key) - { - List parameters = new List(); - - parameters.Add("apikey=" + key); - parameters.Add("priority= " + (int)priority); - parameters.Add("application=" + Global.event_source); - parameters.Add("event=" + source); - parameters.Add("description=" + description); - - WebClient client = new WebClient(); - client.Headers[HttpRequestHeader.ContentType] = "application/x-www-form-urlencoded"; - client.UploadStringAsync(URI, string.Join("&", parameters.ToArray())); - client.UploadStringCompleted += client_UploadStringCompleted; - } - } - - static void client_UploadStringCompleted(object sender, UploadStringCompletedEventArgs e) - { - if(e.Error != null) - Event.WriteError("ProwlNotification", "An error occurred sending notification\r\n" + e.Error.Message); - } - } -} diff --git a/HAILogger/WebNotification.cs b/HAILogger/WebNotification.cs deleted file mode 100644 index 9213e58..0000000 --- a/HAILogger/WebNotification.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; - -namespace HAILogger -{ - static class WebNotification - { - private static List subscriptions = new List(); - private static object subscriptions_lock = new object(); - - public static void AddSubscription(string callback) - { - lock (subscriptions_lock) - { - if (!subscriptions.Contains(callback)) - { - Event.WriteVerbose("WebNotification", "Adding subscription to " + callback); - subscriptions.Add(callback); - } - } - } - - public static void Send(string type, string body) - { - string[] send; - lock (subscriptions_lock) - send = subscriptions.ToArray(); - - foreach (string subscription in send) - { - WebClient client = new WebClient(); - client.Headers.Add(HttpRequestHeader.ContentType, "application/json"); - client.Headers.Add("type", type); - client.UploadStringCompleted += client_UploadStringCompleted; - - try - { - client.UploadStringAsync(new Uri(subscription), "POST", body, subscription); - } - catch (Exception ex) - { - Event.WriteError("WebNotification", "An error occurred sending notification to " + subscription + "\r\n" + ex.ToString()); - subscriptions.Remove(subscription); - } - } - } - - static void client_UploadStringCompleted(object sender, UploadStringCompletedEventArgs e) - { - if (e.Error != null) - { - Event.WriteError("WebNotification", "An error occurred sending notification to " + e.UserState.ToString() + "\r\n" + e.Error.Message); - - lock (subscriptions_lock) - subscriptions.Remove(e.UserState.ToString()); - } - } - } -} \ No newline at end of file diff --git a/HAILogger/WebService.cs b/HAILogger/WebService.cs deleted file mode 100644 index f879cab..0000000 --- a/HAILogger/WebService.cs +++ /dev/null @@ -1,44 +0,0 @@ -using HAI_Shared; -using System; -using System.ServiceModel; -using System.ServiceModel.Description; -using System.ServiceModel.Web; - -namespace HAILogger -{ - public class WebService - { - public static clsHAC HAC; - WebServiceHost host; - - public WebService(clsHAC hac) - { - HAC = hac; - } - - public void Start() - { - Uri uri = new Uri("http://0.0.0.0:" + Global.webapi_port + "/"); - host = new WebServiceHost(typeof(HAIService), uri); - - try - { - ServiceEndpoint ep = host.AddServiceEndpoint(typeof(IHAIService), new WebHttpBinding(), ""); - host.Open(); - - Event.WriteInfo("WebService", "Listening on " + uri.ToString()); - } - catch (CommunicationException ex) - { - Event.WriteError("WebService", "An exception occurred: " + ex.Message); - host.Abort(); - } - } - - public void Stop() - { - if (host != null) - host.Close(); - } - } -} diff --git a/OmniLinkBridge.sln b/OmniLinkBridge.sln new file mode 100644 index 0000000..758f5f0 --- /dev/null +++ b/OmniLinkBridge.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27703.2026 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OmniLinkBridge", "OmniLinkBridge\OmniLinkBridge.csproj", "{0A636707-98BA-45AB-9843-AED430933CEE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OmniLinkBridgeTest", "OmniLinkBridgeTest\OmniLinkBridgeTest.csproj", "{6E6950E4-35F9-4D99-8ADA-B7E2F29D4172}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0A636707-98BA-45AB-9843-AED430933CEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A636707-98BA-45AB-9843-AED430933CEE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A636707-98BA-45AB-9843-AED430933CEE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A636707-98BA-45AB-9843-AED430933CEE}.Release|Any CPU.Build.0 = Release|Any CPU + {6E6950E4-35F9-4D99-8ADA-B7E2F29D4172}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6E6950E4-35F9-4D99-8ADA-B7E2F29D4172}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6E6950E4-35F9-4D99-8ADA-B7E2F29D4172}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6E6950E4-35F9-4D99-8ADA-B7E2F29D4172}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C65BDBEF-1B50-442F-BC26-3A5FE3EDFCA3} + EndGlobalSection +EndGlobal diff --git a/OmniLinkBridge/App.config b/OmniLinkBridge/App.config new file mode 100644 index 0000000..f531d78 --- /dev/null +++ b/OmniLinkBridge/App.config @@ -0,0 +1,37 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OmniLinkBridge/CoreServer.cs b/OmniLinkBridge/CoreServer.cs new file mode 100644 index 0000000..8a8fa3a --- /dev/null +++ b/OmniLinkBridge/CoreServer.cs @@ -0,0 +1,75 @@ +using OmniLinkBridge.Modules; +using OmniLinkBridge.OmniLink; +using log4net; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace OmniLinkBridge +{ + public class CoreServer + { + private static ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + private OmniLinkII omnilink; + private readonly List modules = new List(); + private readonly List tasks = new List(); + + public CoreServer() + { + Thread handler = new Thread(Server); + handler.Start(); + } + + private void Server() + { + Global.running = true; + + log.Debug("Starting up server " + + Assembly.GetExecutingAssembly().GetName().Version.ToString()); + + // Controller connection + modules.Add(omnilink = new OmniLinkII(Global.controller_address, Global.controller_port, Global.controller_key1, Global.controller_key2)); + + // Initialize modules + modules.Add(new LoggerModule(omnilink)); + + if (Global.time_sync) + modules.Add(new TimeSyncModule(omnilink)); + + if (Global.webapi_enabled) + modules.Add(new WebServiceModule(omnilink)); + + if(Global.mqtt_enabled) + modules.Add(new MQTTModule(omnilink)); + + // Startup modules + foreach (IModule module in modules) + { + tasks.Add(Task.Factory.StartNew(() => + { + module.Startup(); + })); + } + + // Wait for all threads to stop + Task.WaitAll(tasks.ToArray()); + } + + public void Shutdown() + { + Global.running = false; + + // Shutdown modules + foreach (IModule module in modules) + module.Shutdown(); + + // Wait for all threads to stop + if (tasks != null) + Task.WaitAll(tasks.ToArray()); + + log.Debug("Shutdown completed"); + } + } +} \ No newline at end of file diff --git a/OmniLinkBridge/Extensions.cs b/OmniLinkBridge/Extensions.cs new file mode 100644 index 0000000..37192ac --- /dev/null +++ b/OmniLinkBridge/Extensions.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Serialization.Json; +using System.Text; + +namespace OmniLinkBridge +{ + public static class Extensions + { + public static double ToCelsius(this double f) + { + // Convert to celsius + return (5.0 / 9.0 * (f - 32)); + } + + public static int ToOmniTemp(this double c) + { + // Convert to omni temp (0 is -40C and 255 is 87.5C) + return (int)Math.Round((c + 40) * 2, 0); + } + + public static bool IsBitSet(this byte b, int pos) + { + return (b & (1 << pos)) != 0; + } + + public static List ParseRanges(this string ranges) + { + string[] groups = ranges.Split(','); + return groups.SelectMany(t => ParseRange(t)).ToList(); + } + + private static List ParseRange(string range) + { + List RangeNums = range + .Split('-') + .Select(t => new String(t.Where(Char.IsDigit).ToArray())) // Digits Only + .Where(t => !string.IsNullOrWhiteSpace(t)) // Only if has a value + .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; + } + } +} diff --git a/OmniLinkBridge/Global.cs b/OmniLinkBridge/Global.cs new file mode 100644 index 0000000..9be454c --- /dev/null +++ b/OmniLinkBridge/Global.cs @@ -0,0 +1,75 @@ +using OmniLinkBridge.MQTT; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net.Mail; + +namespace OmniLinkBridge +{ + public abstract class Global + { + public static bool running; + + // Config File + public static string config_file; + + // HAI / Leviton Omni Controller + public static string controller_address; + public static int controller_port; + public static string controller_key1; + public static string controller_key2; + + // Time Sync + public static bool time_sync; + public static int time_interval; + public static int time_drift; + + // Verbose Console + public static bool verbose_unhandled; + public static bool verbose_event; + public static bool verbose_area; + public static bool verbose_zone; + public static bool verbose_thermostat_timer; + public static bool verbose_thermostat; + public static bool verbose_unit; + public static bool verbose_message; + + // mySQL Logging + public static bool mysql_logging; + public static string mysql_connection; + + // Web Service + public static bool webapi_enabled; + public static int webapi_port; + public static string webapi_subscriptions_file; + + // MQTT + public static bool mqtt_enabled; + public static string mqtt_server; + public static int mqtt_port; + public static string mqtt_username; + public static string mqtt_password; + public static string mqtt_discovery_prefix; + public static HashSet mqtt_discovery_ignore_zones; + public static HashSet mqtt_discovery_ignore_units; + public static ConcurrentDictionary mqtt_discovery_override_zone; + + // Notifications + public static bool notify_area; + public static bool notify_message; + + // Email Notifications + public static string mail_server; + public static int mail_port; + public static string mail_username; + public static string mail_password; + public static MailAddress mail_from; + public static MailAddress[] mail_to; + + // Prowl Notifications + public static string[] prowl_key; + + // Pushover Notifications + public static string pushover_token; + public static string[] pushover_user; + } +} diff --git a/HAILogger/HAI.Controller.dll b/OmniLinkBridge/HAI.Controller.dll similarity index 100% rename from HAILogger/HAI.Controller.dll rename to OmniLinkBridge/HAI.Controller.dll diff --git a/OmniLinkBridge/MQTT/Alarm.cs b/OmniLinkBridge/MQTT/Alarm.cs new file mode 100644 index 0000000..4a3679e --- /dev/null +++ b/OmniLinkBridge/MQTT/Alarm.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OmniLinkBridge.MQTT +{ + public class Alarm : Device + { + public string command_topic { get; set; } + + //public string code { get; set; } = string.Empty; + } +} diff --git a/OmniLinkBridge/MQTT/BinarySensor.cs b/OmniLinkBridge/MQTT/BinarySensor.cs new file mode 100644 index 0000000..4e82da8 --- /dev/null +++ b/OmniLinkBridge/MQTT/BinarySensor.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OmniLinkBridge.MQTT +{ + public class BinarySensor : Device + { + [JsonConverter(typeof(StringEnumConverter))] + public enum DeviceClass + { + battery, + door, + garage_door, + gas, + moisture, + motion, + problem, + smoke, + window + } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public DeviceClass? device_class { get; set; } + } +} diff --git a/OmniLinkBridge/MQTT/Climate.cs b/OmniLinkBridge/MQTT/Climate.cs new file mode 100644 index 0000000..0816864 --- /dev/null +++ b/OmniLinkBridge/MQTT/Climate.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OmniLinkBridge.MQTT +{ + public class Climate : Device + { + public string current_temperature_topic { get; set; } + + public string temperature_low_state_topic { get; set; } + public string temperature_low_command_topic { get; set; } + + public string temperature_high_state_topic { get; set; } + public string temperature_high_command_topic { get; set; } + + public string mode_state_topic { get; set; } + public string mode_command_topic { get; set; } + public List modes { get; set; } = new List(new string[] { "auto", "off", "cool", "heat" }); + + public string fan_mode_state_topic { get; set; } + public string fan_mode_command_topic { get; set; } + public List fan_modes { get; set; } = new List(new string[] { "auto", "on", "cycle" }); + + public string hold_state_topic { get; set; } + public string hold_command_topic { get; set; } + } +} diff --git a/OmniLinkBridge/MQTT/Device.cs b/OmniLinkBridge/MQTT/Device.cs new file mode 100644 index 0000000..3012d37 --- /dev/null +++ b/OmniLinkBridge/MQTT/Device.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OmniLinkBridge.MQTT +{ + public class Device + { + public string name { get; set; } + + public string state_topic { get; set; } + + public string availability_topic { get; set; } = "omnilink/status"; + } +} diff --git a/OmniLinkBridge/MQTT/Light.cs b/OmniLinkBridge/MQTT/Light.cs new file mode 100644 index 0000000..f09862e --- /dev/null +++ b/OmniLinkBridge/MQTT/Light.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OmniLinkBridge.MQTT +{ + public class Light : Device + { + public string command_topic { get; set; } + + public string brightness_state_topic { get; set; } + + public string brightness_command_topic { get; set; } + + public int brightness_scale { get; private set; } = 100; + } +} diff --git a/OmniLinkBridge/MQTT/MappingExtensions.cs b/OmniLinkBridge/MQTT/MappingExtensions.cs new file mode 100644 index 0000000..c13fa8b --- /dev/null +++ b/OmniLinkBridge/MQTT/MappingExtensions.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using HAI_Shared; + +namespace OmniLinkBridge.MQTT +{ + public static class MappingExtensions + { + public static string ToTopic(this clsArea area, Topic topic) + { + return $"omnilink/area{area.Number.ToString()}/{topic.ToString()}"; + } + + public static Alarm ToConfig(this clsArea area) + { + Alarm ret = new Alarm(); + ret.name = area.Name; + ret.state_topic = area.ToTopic(Topic.state); + ret.command_topic = area.ToTopic(Topic.command); + return ret; + } + + public static string ToState(this clsArea area) + { + if (area.AreaBurglaryAlarmText != "OK") + return "triggered"; + else if (area.ExitTimer > 0) + return "pending"; + + switch (area.AreaMode) + { + case enuSecurityMode.Night: + case enuSecurityMode.NightDly: + return "armed_night"; + case enuSecurityMode.Day: + case enuSecurityMode.DayInst: + return "armed_home"; + case enuSecurityMode.Away: + case enuSecurityMode.Vacation: + return "armed_away"; + case enuSecurityMode.Off: + default: + return "disarmed"; + } + } + + public static string ToTopic(this clsZone zone, Topic topic) + { + return $"omnilink/zone{zone.Number.ToString()}/{topic.ToString()}"; + } + + public static Sensor ToConfigTemp(this clsZone zone) + { + Sensor ret = new Sensor(); + ret.name = zone.Name; + ret.device_class = Sensor.DeviceClass.temperature; + ret.state_topic = zone.ToTopic(Topic.state); + ret.unit_of_measurement = "°F"; + return ret; + } + + public static Sensor ToConfigHumidity(this clsZone zone) + { + Sensor ret = new Sensor(); + ret.name = zone.Name; + ret.device_class = Sensor.DeviceClass.humidity; + ret.state_topic = zone.ToTopic(Topic.state); + ret.unit_of_measurement = "%"; + return ret; + } + + public static BinarySensor ToConfig(this clsZone zone) + { + BinarySensor ret = new BinarySensor(); + ret.name = zone.Name; + + Global.mqtt_discovery_override_zone.TryGetValue(zone.Number, out OverrideZone override_zone); + + if (override_zone != null) + { + ret.device_class = override_zone.device_class; + } + else + { + switch (zone.ZoneType) + { + case enuZoneType.EntryExit: + case enuZoneType.X2EntryDelay: + case enuZoneType.X4EntryDelay: + ret.device_class = BinarySensor.DeviceClass.door; + break; + case enuZoneType.Perimeter: + ret.device_class = BinarySensor.DeviceClass.window; + break; + case enuZoneType.Tamper: + ret.device_class = BinarySensor.DeviceClass.problem; + break; + case enuZoneType.AwayInt: + case enuZoneType.NightInt: + ret.device_class = BinarySensor.DeviceClass.motion; + break; + case enuZoneType.Water: + ret.device_class = BinarySensor.DeviceClass.moisture; + break; + case enuZoneType.Fire: + ret.device_class = BinarySensor.DeviceClass.smoke; + break; + case enuZoneType.Gas: + ret.device_class = BinarySensor.DeviceClass.gas; + break; + } + } + + ret.state_topic = zone.ToTopic(Topic.state); + return ret; + } + + public static string ToState(this clsZone zone) + { + if (zone.IsTemperatureZone() || zone.IsHumidityZone()) + return zone.TempText(); + else + return zone.Status.IsBitSet(0) ? "ON" : "OFF"; + } + + public static string ToTopic(this clsUnit unit, Topic topic) + { + return $"omnilink/unit{unit.Number.ToString()}/{topic.ToString()}"; + } + + public static Light ToConfig(this clsUnit unit) + { + Light ret = new Light(); + ret.name = unit.Name; + ret.state_topic = unit.ToTopic(Topic.state); + ret.command_topic = unit.ToTopic(Topic.command); + ret.brightness_state_topic = unit.ToTopic(Topic.brightness_state); + ret.brightness_command_topic = unit.ToTopic(Topic.brightness_command); + return ret; + } + + public static Switch ToConfigSwitch(this clsUnit unit) + { + Switch ret = new Switch(); + ret.name = unit.Name; + ret.state_topic = unit.ToTopic(Topic.state); + ret.command_topic = unit.ToTopic(Topic.command); + return ret; + } + + public static string ToState(this clsUnit unit) + { + return unit.Status == 0 || unit.Status == 100 ? "OFF" : "ON"; + } + + public static int ToBrightnessState(this clsUnit unit) + { + if (unit.Status > 100) + return (ushort)(unit.Status - 100); + else if (unit.Status == 1) + return 100; + else + return 0; + } + + public static string ToTopic(this clsThermostat thermostat, Topic topic) + { + return $"omnilink/thermostat{thermostat.Number.ToString()}/{topic.ToString()}"; + } + + public static Climate ToConfig(this clsThermostat thermostat) + { + Climate ret = new Climate(); + ret.name = thermostat.Name; + 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; + } + + public static string ToOperationState(this clsThermostat thermostat) + { + string status = thermostat.HorC_StatusText(); + + if (status.Contains("COOLING")) + return "cool"; + else if (status.Contains("HEATING")) + return "heat"; + else + return "idle"; + } + + public static string ToTopic(this clsButton button, Topic topic) + { + return $"omnilink/button{button.Number.ToString()}/{topic.ToString()}"; + } + + public static Switch ToConfig(this clsButton button) + { + Switch ret = new Switch(); + ret.name = button.Name; + ret.state_topic = button.ToTopic(Topic.state); + ret.command_topic = button.ToTopic(Topic.command); + return ret; + } + } +} diff --git a/OmniLinkBridge/MQTT/OverrideZone.cs b/OmniLinkBridge/MQTT/OverrideZone.cs new file mode 100644 index 0000000..e7492d4 --- /dev/null +++ b/OmniLinkBridge/MQTT/OverrideZone.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OmniLinkBridge.MQTT +{ + public class OverrideZone + { + public BinarySensor.DeviceClass device_class { get; set; } + } +} diff --git a/OmniLinkBridge/MQTT/Sensor.cs b/OmniLinkBridge/MQTT/Sensor.cs new file mode 100644 index 0000000..bda9a03 --- /dev/null +++ b/OmniLinkBridge/MQTT/Sensor.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OmniLinkBridge.MQTT +{ + public class Sensor : Device + { + [JsonConverter(typeof(StringEnumConverter))] + public enum DeviceClass + { + humidity, + temperature + } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public DeviceClass? device_class { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string unit_of_measurement { get; set; } + } +} diff --git a/OmniLinkBridge/MQTT/Switch.cs b/OmniLinkBridge/MQTT/Switch.cs new file mode 100644 index 0000000..f44c8a7 --- /dev/null +++ b/OmniLinkBridge/MQTT/Switch.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OmniLinkBridge.MQTT +{ + public class Switch : Device + { + public string command_topic { get; set; } + } +} diff --git a/OmniLinkBridge/MQTT/Topics.cs b/OmniLinkBridge/MQTT/Topics.cs new file mode 100644 index 0000000..8f759e0 --- /dev/null +++ b/OmniLinkBridge/MQTT/Topics.cs @@ -0,0 +1,56 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OmniLinkBridge.MQTT +{ + public class Topic + { + public string Value { get; private set; } + + private Topic(string value) + { + Value = value; + } + + public override string ToString() + { + return Value; + } + + public static Topic state { get { return new Topic("state"); } } + public static Topic command { get { return new Topic("command"); } } + + public static Topic brightness_state { get { return new Topic("brightness_state"); } } + public static Topic brightness_command { get { return new Topic("brightness_command"); } } + + public static Topic current_operation { get { return new Topic("current_operation"); } } + public static Topic current_temperature { get { return new Topic("current_temperature"); } } + public static Topic current_humidity { get { return new Topic("current_humidity"); } } + + public static Topic temperature_heat_state { get { return new Topic("temperature_heat_state"); } } + public static Topic temperature_heat_command { get { return new Topic("temperature_heat_command"); } } + + public static Topic temperature_cool_state { get { return new Topic("temperature_cool_state"); } } + public static Topic temperature_cool_command { get { return new Topic("temperature_cool_command"); } } + + public static Topic humidify_state { get { return new Topic("humidify_state"); } } + public static Topic humidify_command { get { return new Topic("humidify_command"); } } + + public static Topic dehumidify_state { get { return new Topic("dehumidify_state"); } } + public static Topic dehumidify_command { get { return new Topic("dehumidify_command"); } } + + public static Topic mode_state { get { return new Topic("mode_state"); } } + public static Topic mode_command { get { return new Topic("mode_command"); } } + + public static Topic fan_mode_state { get { return new Topic("fan_mode_state"); } } + public static Topic fan_mode_command { get { return new Topic("fan_mode_command"); } } + + public static Topic hold_state { get { return new Topic("hold_state"); } } + public static Topic hold_command { get { return new Topic("hold_command"); } } + } +} diff --git a/OmniLinkBridge/Modules/IModule.cs b/OmniLinkBridge/Modules/IModule.cs new file mode 100644 index 0000000..f71701c --- /dev/null +++ b/OmniLinkBridge/Modules/IModule.cs @@ -0,0 +1,8 @@ +namespace OmniLinkBridge.Modules +{ + interface IModule + { + void Startup(); + void Shutdown(); + } +} diff --git a/OmniLinkBridge/Modules/LoggerModule.cs b/OmniLinkBridge/Modules/LoggerModule.cs new file mode 100644 index 0000000..e2327e4 --- /dev/null +++ b/OmniLinkBridge/Modules/LoggerModule.cs @@ -0,0 +1,344 @@ +using OmniLinkBridge.Notifications; +using OmniLinkBridge.OmniLink; +using log4net; +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Odbc; +using System.Reflection; +using System.Threading; + +namespace OmniLinkBridge.Modules +{ + public class LoggerModule : IModule + { + private static ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + private OmniLinkII omnilink; + private 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 AutoResetEvent trigger = new AutoResetEvent(false); + + public LoggerModule(OmniLinkII omni) + { + omnilink = omni; + omnilink.OnAreaStatus += Omnilink_OnAreaStatus; + omnilink.OnZoneStatus += Omnilink_OnZoneStatus; + omnilink.OnThermostatStatus += Omnilink_OnThermostatStatus; + omnilink.OnUnitStatus += Omnilink_OnUnitStatus; + omnilink.OnMessageStatus += Omnilink_OnMessageStatus; + omnilink.OnSystemStatus += Omnilink_OnSystemStatus; + } + + public void Startup() + { + if (Global.mysql_logging) + { + log.Info("Connecting to database"); + + mysql_conn = new OdbcConnection(Global.mysql_connection); + + // Must make an initial connection + if (!DBOpen()) + Environment.Exit(1); + } + + while (true) + { + // End gracefully when not logging or database queue empty + if (!Global.running && (!Global.mysql_logging || DBQueueCount() == 0)) + break; + + // Make sure database connection is active + if (Global.mysql_logging && mysql_conn.State != ConnectionState.Open) + { + // Nothing we can do if shutting down + if (!Global.running) + break; + + if (mysql_retry < DateTime.Now) + DBOpen(); + + if (mysql_conn.State != ConnectionState.Open) + { + // Loop to prevent database queries from executing + trigger.WaitOne(new TimeSpan(0, 0, 1)); + continue; + } + } + + // Sleep when not logging or database queue empty + if (!Global.mysql_logging || DBQueueCount() == 0) + { + trigger.WaitOne(new TimeSpan(0, 0, 1)); + continue; + } + + // Grab a copy in case the database query fails + string query; + lock (mysql_lock) + query = mysql_queue.Peek(); + + try + { + // Execute the database query + mysql_command = new OdbcCommand(query, mysql_conn); + mysql_command.ExecuteNonQuery(); + + // Successful remove query from queue + lock (mysql_lock) + mysql_queue.Dequeue(); + } + catch (Exception ex) + { + if (mysql_conn.State != ConnectionState.Open) + { + log.Warn("Lost connection to database"); + } + else + { + log.Error("Error executing query\r\n" + query, ex); + + // Prevent an endless loop from failed query + lock (mysql_lock) + mysql_queue.Dequeue(); + } + } + } + + if (Global.mysql_logging) + DBClose(); + } + + public void Shutdown() + { + trigger.Set(); + } + + private void Omnilink_OnAreaStatus(object sender, AreaStatusEventArgs e) + { + // Alarm notifcation + if (e.Area.AreaFireAlarmText != "OK") + { + Notification.Notify("ALARM", "FIRE " + e.Area.Name + " " + e.Area.AreaFireAlarmText, NotificationPriority.Emergency); + + if (!alarms.Contains("FIRE" + e.ID)) + alarms.Add("FIRE" + e.ID); + } + else if (alarms.Contains("FIRE" + e.ID)) + { + Notification.Notify("ALARM CLEARED", "FIRE " + e.Area.Name + " " + e.Area.AreaFireAlarmText, NotificationPriority.High); + + alarms.Remove("FIRE" + e.ID); + } + + if (e.Area.AreaBurglaryAlarmText != "OK") + { + Notification.Notify("ALARM", "BURGLARY " + e.Area.Name + " " + e.Area.AreaBurglaryAlarmText, NotificationPriority.Emergency); + + if (!alarms.Contains("BURGLARY" + e.ID)) + alarms.Add("BURGLARY" + e.ID); + } + else if (alarms.Contains("BURGLARY" + e.ID)) + { + Notification.Notify("ALARM CLEARED", "BURGLARY " + e.Area.Name + " " + e.Area.AreaBurglaryAlarmText, NotificationPriority.High); + + alarms.Remove("BURGLARY" + e.ID); + } + + if (e.Area.AreaAuxAlarmText != "OK") + { + Notification.Notify("ALARM", "AUX " + e.Area.Name + " " + e.Area.AreaAuxAlarmText, NotificationPriority.Emergency); + + if (!alarms.Contains("AUX" + e.ID)) + alarms.Add("AUX" + e.ID); + } + else if (alarms.Contains("AUX" + e.ID)) + { + Notification.Notify("ALARM CLEARED", "AUX " + e.Area.Name + " " + e.Area.AreaAuxAlarmText, NotificationPriority.High); + + alarms.Remove("AUX" + e.ID); + } + + if (e.Area.AreaDuressAlarmText != "OK") + { + Notification.Notify("ALARM", "DURESS " + e.Area.Name + " " + e.Area.AreaDuressAlarmText, NotificationPriority.Emergency); + + if (!alarms.Contains("DURESS" + e.ID)) + alarms.Add("DURESS" + e.ID); + } + else if (alarms.Contains("DURESS" + e.ID)) + { + Notification.Notify("ALARM CLEARED", "DURESS " + e.Area.Name + " " + e.Area.AreaDuressAlarmText, NotificationPriority.High); + + alarms.Remove("DURESS" + e.ID); + } + + string status = e.Area.ModeText(); + + if (e.Area.ExitTimer > 0) + status = "ARMING " + status; + + if (e.Area.EntryTimer > 0) + status = "TRIPPED " + status; + + DBQueue(@" + INSERT INTO log_areas (timestamp, e.AreaID, name, + fire, police, auxiliary, + duress, security) + VALUES ('" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "','" + e.ID.ToString() + "','" + e.Area.Name + "','" + + e.Area.AreaFireAlarmText + "','" + e.Area.AreaBurglaryAlarmText + "','" + e.Area.AreaAuxAlarmText + "','" + + e.Area.AreaDuressAlarmText + "','" + status + "')"); + + if (Global.verbose_area) + log.Debug("AreaStatus " + e.ID + " " + e.Area.Name + ", Status: " + status); + + if (Global.notify_area && e.Area.LastMode != e.Area.AreaMode) + Notification.Notify("Security", e.Area.Name + " " + e.Area.ModeText()); + } + + private void Omnilink_OnZoneStatus(object sender, ZoneStatusEventArgs e) + { + DBQueue(@" + INSERT INTO log_zones (timestamp, id, name, status) + VALUES ('" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "','" + e.ID + "','" + e.Zone.Name + "','" + e.Zone.StatusText() + "')"); + + if (Global.verbose_zone) + { + if (e.Zone.IsTemperatureZone()) + log.Debug("ZoneStatus " + e.ID + " " + e.Zone.Name + ", Temp: " + e.Zone.TempText()); + else + log.Debug("ZoneStatus" + e.ID + " " + e.Zone.Name + ", Status: " + e.Zone.StatusText()); + } + } + + 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); + + DBQueue(@" + INSERT INTO log_thermostats (timestamp, id, name, + status, temp, heat, cool, + humidity, humidify, dehumidify, + mode, fan, hold) + VALUES ('" + DateTime.Now.ToString("yyyy-MM-dd HH:mm") + "','" + e.ID + "','" + e.Thermostat.Name + "','" + + e.Thermostat.HorC_StatusText() + "','" + temp.ToString() + "','" + heat + "','" + cool + "','" + + humidity + "','" + humidify + "','" + dehumidify + "','" + + e.Thermostat.ModeText() + "','" + e.Thermostat.FanModeText() + "','" + e.Thermostat.HoldStatusText() + "')"); + + if (Global.verbose_thermostat) + log.Debug("ThermostatStatus " + e.ID + " " + e.Thermostat.Name + + ", Status: " + e.Thermostat.TempText() + " " + e.Thermostat.HorC_StatusText() + + ", Heat: " + e.Thermostat.HeatSetpointText() + + ", Cool: " + e.Thermostat.CoolSetpointText() + + ", Mode: " + e.Thermostat.ModeText() + + ", Fan: " + e.Thermostat.FanModeText() + + ", Hold: " + e.Thermostat.HoldStatusText()); + } + + private void Omnilink_OnUnitStatus(object sender, UnitStatusEventArgs e) + { + string status = e.Unit.StatusText; + + if (e.Unit.Status == 100 && e.Unit.StatusTime == 0) + status = "OFF"; + else if (e.Unit.Status == 200 && e.Unit.StatusTime == 0) + status = "ON"; + + DBQueue(@" + INSERT INTO log_e.Units (timestamp, id, name, + status, statusvalue, statustime) + VALUES ('" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "','" + e.ID + "','" + e.Unit.Name + "','" + + status + "','" + e.Unit.Status + "','" + e.Unit.StatusTime + "')"); + + if (Global.verbose_unit) + log.Debug("UnitStatus " + e.ID + " " + e.Unit.Name + ", Status: " + status); + } + + private void Omnilink_OnMessageStatus(object sender, MessageStatusEventArgs e) + { + DBQueue(@" + INSERT INTO log_messages (timestamp, id, name, status) + VALUES ('" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "','" + e.ID + "','" + e.Message.Name + "','" + e.Message.StatusText() + "')"); + + if (Global.verbose_message) + log.Debug("MessageStatus " + e.Message.Name + ", " + e.Message.StatusText()); + + if (Global.notify_message) + Notification.Notify("Message", e.ID + " " + e.Message.Name + ", " + e.Message.StatusText()); + } + + private void Omnilink_OnSystemStatus(object sender, SystemStatusEventArgs e) + { + DBQueue(@" + INSERT INTO log_events (timestamp, name, status) + VALUES ('" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "','" + e.Type.ToString() + "','" + e.Value + "')"); + + if (Global.verbose_event) + log.Debug("SystemEvent " + e.Type.ToString() + " " + e.Value); + + if (e.SendNotification) + Notification.Notify("SystemEvent", e.Type.ToString() + " " + e.Value); + } + + public bool DBOpen() + { + try + { + if (mysql_conn.State != ConnectionState.Open) + mysql_conn.Open(); + + mysql_retry = DateTime.MinValue; + } + catch (Exception ex) + { + log.Error("Failed to connect to database", ex); + mysql_retry = DateTime.Now.AddMinutes(1); + return false; + } + + return true; + } + + public void DBClose() + { + if (mysql_conn.State != ConnectionState.Closed) + mysql_conn.Close(); + } + + public void DBQueue(string query) + { + if (!Global.mysql_logging) + return; + + lock (mysql_lock) + mysql_queue.Enqueue(query); + } + + private int DBQueueCount() + { + int count; + lock (mysql_lock) + count = mysql_queue.Count; + + return count; + } + } +} diff --git a/OmniLinkBridge/Modules/MQTTModule.cs b/OmniLinkBridge/Modules/MQTTModule.cs new file mode 100644 index 0000000..32157fb --- /dev/null +++ b/OmniLinkBridge/Modules/MQTTModule.cs @@ -0,0 +1,422 @@ +using HAI_Shared; +using OmniLinkBridge.OmniLink; +using log4net; +using MQTTnet; +using MQTTnet.Client; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using Newtonsoft.Json; +using MQTTnet.Extensions.ManagedClient; +using OmniLinkBridge.MQTT; +using MQTTnet.Protocol; +using System.Text.RegularExpressions; +using System.Text; + +namespace OmniLinkBridge.Modules +{ + public class MQTTModule : IModule + { + private static ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + private OmniLinkII OmniLink { get; set; } + private IManagedMqttClient MqttClient { get; set; } + + private Regex regexTopic = new Regex("omnilink/([A-Za-z]+)([0-9]+)/(.*)", RegexOptions.Compiled); + + private readonly AutoResetEvent trigger = new AutoResetEvent(false); + + public MQTTModule(OmniLinkII omni) + { + OmniLink = omni; + OmniLink.OnConnect += OmniLink_OnConnect; + OmniLink.OnAreaStatus += Omnilink_OnAreaStatus; + OmniLink.OnZoneStatus += Omnilink_OnZoneStatus; + OmniLink.OnUnitStatus += Omnilink_OnUnitStatus; + OmniLink.OnThermostatStatus += Omnilink_OnThermostatStatus; + } + + public void Startup() + { + MqttClientOptionsBuilder options = new MqttClientOptionsBuilder() + .WithTcpServer(Global.mqtt_server); + + if (!string.IsNullOrEmpty(Global.mqtt_username)) + options = options + .WithCredentials(Global.mqtt_username, Global.mqtt_password); + + ManagedMqttClientOptions manoptions = new ManagedMqttClientOptionsBuilder() + .WithAutoReconnectDelay(TimeSpan.FromSeconds(5)) + .WithClientOptions(options.Build()) + .Build(); + + MqttClient = new MqttFactory().CreateManagedMqttClient(); + MqttClient.Connected += (sender, e) => { log.Debug("Connected"); }; + MqttClient.ConnectingFailed += (sender, e) => { log.Debug("Error " + e.Exception.Message); }; + + MqttClient.StartAsync(manoptions); + + MqttClient.ApplicationMessageReceived += MqttClient_ApplicationMessageReceived; + + MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic("omnilink/+/" + Topic.command).Build()); + MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic("omnilink/+/" + Topic.brightness_command).Build()); + MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic("omnilink/+/" + Topic.temperature_heat_command).Build()); + MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic("omnilink/+/" + Topic.temperature_cool_command).Build()); + MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic("omnilink/+/" + Topic.humidify_command).Build()); + MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic("omnilink/+/" + Topic.dehumidify_command).Build()); + MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic("omnilink/+/" + Topic.mode_command).Build()); + MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic("omnilink/+/" + Topic.fan_mode_command).Build()); + MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic("omnilink/+/" + Topic.hold_command).Build()); + + // Wait until shutdown + trigger.WaitOne(); + + MqttClient.PublishAsync("omnilink/status", "offline", MqttQualityOfServiceLevel.AtMostOnce, true); + } + + private void MqttClient_ApplicationMessageReceived(object sender, MqttApplicationMessageReceivedEventArgs e) + { + Match match = regexTopic.Match(e.ApplicationMessage.Topic); + + if (!match.Success) + return; + + string payload = Encoding.UTF8.GetString(e.ApplicationMessage.Payload); + + log.Debug($"Received: Type: {match.Groups[1].Value}, Id: {match.Groups[2].Value}, Command: {match.Groups[3].Value}, Value: {payload}"); + + if (match.Groups[1].Value == "area" && ushort.TryParse(match.Groups[2].Value, out ushort areaId) && areaId < OmniLink.Controller.Areas.Count) + { + ProcessAreaReceived(OmniLink.Controller.Areas[areaId], match.Groups[3].Value, payload); + } + else if (match.Groups[1].Value == "unit" && ushort.TryParse(match.Groups[2].Value, out ushort unitId) && unitId < OmniLink.Controller.Units.Count) + { + ProcessUnitReceived(OmniLink.Controller.Units[unitId], match.Groups[3].Value, payload); + } + else if (match.Groups[1].Value == "thermostat" && ushort.TryParse(match.Groups[2].Value, out ushort thermostatId) && thermostatId < OmniLink.Controller.Thermostats.Count) + { + ProcessThermostatReceived(OmniLink.Controller.Thermostats[thermostatId], match.Groups[3].Value, payload); + } + else if (match.Groups[1].Value == "button" && ushort.TryParse(match.Groups[2].Value, out ushort buttonId) && buttonId < OmniLink.Controller.Buttons.Count) + { + ProcessButtonReceived(OmniLink.Controller.Buttons[buttonId], match.Groups[3].Value, payload); + } + } + + private void ProcessAreaReceived(clsArea area, string command, string payload) + { + if (string.Compare(command, Topic.command.ToString()) == 0) + { + switch(payload) + { + case "ARM_HOME": + log.Debug("SetArea: " + area.Number + " to home"); + OmniLink.Controller.SendCommand(enuUnitCommand.SecurityDay, 0, (ushort)area.Number); + break; + case "ARM_AWAY": + log.Debug("SetArea: " + area.Number + " to away"); + OmniLink.Controller.SendCommand(enuUnitCommand.SecurityAway, 0, (ushort)area.Number); + break; + case "ARM_NIGHT": + log.Debug("SetArea: " + area.Number + " to night"); + OmniLink.Controller.SendCommand(enuUnitCommand.SecurityNight, 0, (ushort)area.Number); + break; + case "DISARM": + log.Debug("SetArea: " + area.Number + " to disarm"); + OmniLink.Controller.SendCommand(enuUnitCommand.SecurityOff, 0, (ushort)area.Number); + break; + + // The below aren't supported by Home Assistant + case "ARM_HOME_INSTANT": + log.Debug("SetArea: " + area.Number + " to home instant"); + OmniLink.Controller.SendCommand(enuUnitCommand.SecurityDyi, 0, (ushort)area.Number); + break; + case "ARM_NIGHT_DELAY": + log.Debug("SetArea: " + area.Number + " to night delay"); + OmniLink.Controller.SendCommand(enuUnitCommand.SecurityNtd, 0, (ushort)area.Number); + break; + case "ARM_VACATION": + log.Debug("SetArea: " + area.Number + " to vacation"); + OmniLink.Controller.SendCommand(enuUnitCommand.SecurityVac, 0, (ushort)area.Number); + break; + } + } + } + + private void ProcessUnitReceived(clsUnit unit, string command, string payload) + { + if (string.Compare(command, Topic.command.ToString()) == 0 && (payload == "ON" || payload == "OFF")) + { + if (unit.ToState() != payload) + { + log.Debug("SetUnit: " + unit.Number + " to " + payload); + + if (payload == "ON") + OmniLink.Controller.SendCommand(enuUnitCommand.On, 0, (ushort)unit.Number); + else + OmniLink.Controller.SendCommand(enuUnitCommand.Off, 0, (ushort)unit.Number); + } + } + else if (string.Compare(command, Topic.brightness_command.ToString()) == 0 && Int32.TryParse(payload, out int unitValue)) + { + log.Debug("SetUnit: " + unit.Number + " to " + payload + "%"); + + OmniLink.Controller.SendCommand(enuUnitCommand.Level, BitConverter.GetBytes(unitValue)[0], (ushort)unit.Number); + + // Force status change instead of waiting on controller to update + // Home Assistant sends brightness immediately followed by ON, + // which will cause light to go to 100% brightness + unit.Status = (byte)(100 + unitValue); + } + } + + private void ProcessThermostatReceived(clsThermostat thermostat, string command, string payload) + { + if (string.Compare(command, Topic.temperature_heat_command.ToString()) == 0 && double.TryParse(payload, out double tempLow)) + { + int temp = tempLow.ToCelsius().ToOmniTemp(); + log.Debug("SetThermostatHeatSetpoint: " + thermostat.Number + " to " + payload + "F (" + temp + ")"); + OmniLink.Controller.SendCommand(enuUnitCommand.SetLowSetPt, BitConverter.GetBytes(temp)[0], (ushort)thermostat.Number); + } + else if (string.Compare(command, Topic.temperature_cool_command.ToString()) == 0 && double.TryParse(payload, out double tempHigh)) + { + int temp = tempHigh.ToCelsius().ToOmniTemp(); + log.Debug("SetThermostatCoolSetpoint: " + thermostat.Number + " to " + payload + "F (" + temp + ")"); + OmniLink.Controller.SendCommand(enuUnitCommand.SetHighSetPt, BitConverter.GetBytes(temp)[0], (ushort)thermostat.Number); + } + else if (string.Compare(command, Topic.humidify_command.ToString()) == 0 && double.TryParse(payload, out double humidify)) + { + int level = humidify.ToCelsius().ToOmniTemp(); + log.Debug("SetThermostatHumidifySetpoint: " + thermostat.Number + " to " + payload + "% (" + level + ")"); + OmniLink.Controller.SendCommand(enuUnitCommand.SetHumidifySetPt, BitConverter.GetBytes(level)[0], (ushort)thermostat.Number); + } + else if (string.Compare(command, Topic.dehumidify_command.ToString()) == 0 && double.TryParse(payload, out double dehumidify)) + { + int level = dehumidify.ToCelsius().ToOmniTemp(); + log.Debug("SetThermostatDehumidifySetpoint: " + thermostat.Number + " to " + payload + "% (" + level + ")"); + OmniLink.Controller.SendCommand(enuUnitCommand.SetDeHumidifySetPt, BitConverter.GetBytes(level)[0], (ushort)thermostat.Number); + } + else if (string.Compare(command, Topic.mode_command.ToString()) == 0 && Enum.TryParse(payload, true, out enuThermostatMode mode)) + { + log.Debug("SetThermostatMode: " + thermostat.Number + " to " + payload); + OmniLink.Controller.SendCommand(enuUnitCommand.Mode, BitConverter.GetBytes((int)mode)[0], (ushort)thermostat.Number); + } + else if (string.Compare(command, Topic.fan_mode_command.ToString()) == 0 && Enum.TryParse(payload, true, out enuThermostatFanMode fanMode)) + { + log.Debug("SetThermostatFanMode: " + thermostat.Number + " to " + payload); + OmniLink.Controller.SendCommand(enuUnitCommand.Fan, BitConverter.GetBytes((int)fanMode)[0], (ushort)thermostat.Number); + } + else if (string.Compare(command, Topic.hold_command.ToString()) == 0 && Enum.TryParse(payload, true, out enuThermostatHoldMode holdMode)) + { + log.Debug("SetThermostatHold: " + thermostat.Number + " to " + payload); + OmniLink.Controller.SendCommand(enuUnitCommand.Hold, BitConverter.GetBytes((int)holdMode)[0], (ushort)thermostat.Number); + } + } + + private void ProcessButtonReceived(clsButton button, string command, string payload) + { + if (string.Compare(command, Topic.command.ToString()) == 0 && payload == "ON") + { + log.Debug("PushButton: " + button.Number); + OmniLink.Controller.SendCommand(enuUnitCommand.Button, 0, (ushort)button.Number); + } + } + + public void Shutdown() + { + trigger.Set(); + } + + private void OmniLink_OnConnect(object sender, EventArgs e) + { + PublishConfig(); + + MqttClient.PublishAsync("omnilink/status", "online", MqttQualityOfServiceLevel.AtMostOnce, true); + } + + private void PublishConfig() + { + PublishAreas(); + PublishZones(); + PublishUnits(); + PublishThermostats(); + PublishButtons(); + } + + private void PublishAreas() + { + log.Debug("Publishing areas"); + + for (ushort i = 1; i < OmniLink.Controller.Areas.Count; i++) + { + clsArea area = OmniLink.Controller.Areas[i]; + + if (area.DefaultProperties == true) + { + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/alarm_control_panel/omnilink/area{i.ToString()}/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); + continue; + } + + PublishAreaState(area); + + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/alarm_control_panel/omnilink/area{i.ToString()}/config", + JsonConvert.SerializeObject(area.ToConfig()), MqttQualityOfServiceLevel.AtMostOnce, true); + } + } + + private void PublishZones() + { + log.Debug("Publishing zones"); + + for (ushort i = 1; i < OmniLink.Controller.Zones.Count; i++) + { + clsZone zone = OmniLink.Controller.Zones[i]; + + if (zone.DefaultProperties == true || Global.mqtt_discovery_ignore_zones.Contains(zone.Number)) + { + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/omnilink/zone{i.ToString()}/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/omnilink/zone{i.ToString()}/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); + continue; + } + + PublishZoneState(zone); + + if (zone.IsTemperatureZone()) + { + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/omnilink/zone{i.ToString()}/config", + JsonConvert.SerializeObject(zone.ToConfigTemp()), MqttQualityOfServiceLevel.AtMostOnce, true); + } + else if (zone.IsHumidityZone()) + { + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/omnilink/zone{i.ToString()}/config", + JsonConvert.SerializeObject(zone.ToConfigHumidity()), MqttQualityOfServiceLevel.AtMostOnce, true); + } + else + { + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/omnilink/zone{i.ToString()}/config", + JsonConvert.SerializeObject(zone.ToConfig()), MqttQualityOfServiceLevel.AtMostOnce, true); + } + } + } + + private void PublishUnits() + { + log.Debug("Publishing units"); + + for (ushort i = 1; i < OmniLink.Controller.Units.Count; i++) + { + string type = i < 385 ? "light" : "switch"; + + clsUnit unit = OmniLink.Controller.Units[i]; + + if (unit.DefaultProperties == true || Global.mqtt_discovery_ignore_units.Contains(unit.Number)) + { + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/{type}/omnilink/unit{i.ToString()}/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); + continue; + } + + PublishUnitState(unit); + + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/{type}/omnilink/unit{i.ToString()}/config", + JsonConvert.SerializeObject(unit.ToConfig()), MqttQualityOfServiceLevel.AtMostOnce, true); + } + } + + private void PublishThermostats() + { + log.Debug("Publishing thermostats"); + + for (ushort i = 1; i < OmniLink.Controller.Thermostats.Count; i++) + { + clsThermostat thermostat = OmniLink.Controller.Thermostats[i]; + + if (thermostat.DefaultProperties == true) + { + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/climate/omnilink/thermostat{i.ToString()}/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); + continue; + } + + PublishThermostatState(thermostat); + + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/climate/omnilink/thermostat{i.ToString()}/config", + JsonConvert.SerializeObject(thermostat.ToConfig()), MqttQualityOfServiceLevel.AtMostOnce, true); + } + } + + private void PublishButtons() + { + log.Debug("Publishing buttons"); + + for (ushort i = 1; i < OmniLink.Controller.Buttons.Count; i++) + { + clsButton button = OmniLink.Controller.Buttons[i]; + + if (button.DefaultProperties == true) + { + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/switch/omnilink/button{i.ToString()}/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); + continue; + } + + // Buttons are always off + MqttClient.PublishAsync(button.ToTopic(Topic.state), "OFF", MqttQualityOfServiceLevel.AtMostOnce, true); + + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/switch/omnilink/button{i.ToString()}/config", + JsonConvert.SerializeObject(button.ToConfig()), MqttQualityOfServiceLevel.AtMostOnce, true); + } + } + + private void Omnilink_OnAreaStatus(object sender, AreaStatusEventArgs e) + { + PublishAreaState(e.Area); + } + + private void Omnilink_OnZoneStatus(object sender, ZoneStatusEventArgs e) + { + PublishZoneState(e.Zone); + } + + private void Omnilink_OnUnitStatus(object sender, UnitStatusEventArgs e) + { + PublishUnitState(e.Unit); + } + + private void Omnilink_OnThermostatStatus(object sender, ThermostatStatusEventArgs e) + { + if(!e.EventTimer) + PublishThermostatState(e.Thermostat); + } + + private void PublishAreaState(clsArea area) + { + MqttClient.PublishAsync(area.ToTopic(Topic.state), area.ToState(), MqttQualityOfServiceLevel.AtMostOnce, true); + } + + private void PublishZoneState(clsZone zone) + { + MqttClient.PublishAsync(zone.ToTopic(Topic.state), zone.ToState(), MqttQualityOfServiceLevel.AtMostOnce, true); + } + + private void PublishUnitState(clsUnit unit) + { + MqttClient.PublishAsync(unit.ToTopic(Topic.state), unit.ToState(), MqttQualityOfServiceLevel.AtMostOnce, true); + + if(unit.Number < 385) + MqttClient.PublishAsync(unit.ToTopic(Topic.brightness_state), unit.ToBrightnessState().ToString(), MqttQualityOfServiceLevel.AtMostOnce, true); + } + + private void PublishThermostatState(clsThermostat thermostat) + { + MqttClient.PublishAsync(thermostat.ToTopic(Topic.current_operation), thermostat.ToOperationState(), MqttQualityOfServiceLevel.AtMostOnce, true); + MqttClient.PublishAsync(thermostat.ToTopic(Topic.current_temperature), thermostat.TempText(), MqttQualityOfServiceLevel.AtMostOnce, true); + MqttClient.PublishAsync(thermostat.ToTopic(Topic.current_humidity), thermostat.HumidityText(), MqttQualityOfServiceLevel.AtMostOnce, true); + MqttClient.PublishAsync(thermostat.ToTopic(Topic.temperature_heat_state), thermostat.HeatSetpointText(), MqttQualityOfServiceLevel.AtMostOnce, true); + MqttClient.PublishAsync(thermostat.ToTopic(Topic.temperature_cool_state), thermostat.CoolSetpointText(), MqttQualityOfServiceLevel.AtMostOnce, true); + MqttClient.PublishAsync(thermostat.ToTopic(Topic.humidify_state), thermostat.HumidifySetpointText(), MqttQualityOfServiceLevel.AtMostOnce, true); + MqttClient.PublishAsync(thermostat.ToTopic(Topic.dehumidify_state), thermostat.DehumidifySetpointText(), MqttQualityOfServiceLevel.AtMostOnce, true); + MqttClient.PublishAsync(thermostat.ToTopic(Topic.mode_state), thermostat.ModeText().ToLower(), MqttQualityOfServiceLevel.AtMostOnce, true); + MqttClient.PublishAsync(thermostat.ToTopic(Topic.fan_mode_state), thermostat.FanModeText().ToLower(), MqttQualityOfServiceLevel.AtMostOnce, true); + MqttClient.PublishAsync(thermostat.ToTopic(Topic.hold_state), thermostat.HoldStatusText().ToLower(), MqttQualityOfServiceLevel.AtMostOnce, true); + } + } +} diff --git a/OmniLinkBridge/Modules/OmniLinkII.cs b/OmniLinkBridge/Modules/OmniLinkII.cs new file mode 100644 index 0000000..b7cfba7 --- /dev/null +++ b/OmniLinkBridge/Modules/OmniLinkII.cs @@ -0,0 +1,837 @@ +using HAI_Shared; +using OmniLinkBridge.OmniLink; +using log4net; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace OmniLinkBridge.Modules +{ + public class OmniLinkII : IModule + { + private static 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(); + + // Events + public event EventHandler OnConnect; + public event EventHandler OnAreaStatus; + public event EventHandler OnZoneStatus; + public event EventHandler OnThermostatStatus; + public event EventHandler OnUnitStatus; + public event EventHandler OnMessageStatus; + public event EventHandler OnSystemStatus; + + private readonly AutoResetEvent trigger = new AutoResetEvent(false); + private readonly AutoResetEvent nameWait = new AutoResetEvent(false); + + public OmniLinkII(string address, int port, string key1, string key2) + { + Controller = new clsHAC(); + + Controller.Connection.NetworkAddress = address; + Controller.Connection.NetworkPort = (ushort)port; + Controller.Connection.ControllerKey = clsUtil.HexString2ByteArray(String.Concat(key1, key2)); + + Controller.PreferredNetworkProtocol = clsHAC.enuPreferredNetworkProtocol.TCP; + Controller.Connection.ConnectionType = enuOmniLinkConnectionType.Network_TCP; + + tstat_timer.Elapsed += tstat_timer_Elapsed; + tstat_timer.AutoReset = false; + } + + public void Startup() + { + while (Global.running) + { + // Make sure controller connection is active + if (Controller.Connection.ConnectionState == enuOmniLinkConnectionState.Offline && + retry < DateTime.Now) + { + Connect(); + } + + trigger.WaitOne(new TimeSpan(0, 0, 5)); + } + } + + public void Shutdown() + { + trigger.Set(); + } + + #region Connection + private void Connect() + { + if (Controller.Connection.ConnectionState == enuOmniLinkConnectionState.Offline) + { + retry = DateTime.Now.AddMinutes(1); + + Controller.Connection.Connect(HandleConnectStatus, HandleUnsolicitedPackets); + } + } + + private void Disconnect() + { + if (Controller.Connection.ConnectionState != enuOmniLinkConnectionState.Offline) + Controller.Connection.Disconnect(); + } + + private void HandleConnectStatus(enuOmniLinkCommStatus CS) + { + switch (CS) + { + case enuOmniLinkCommStatus.NoReply: + log.Error("CONNECTION STATUS: No Reply"); + break; + case enuOmniLinkCommStatus.UnrecognizedReply: + log.Error("CONNECTION STATUS: Unrecognized Reply"); + break; + case enuOmniLinkCommStatus.UnsupportedProtocol: + log.Error("CONNECTION STATUS: Unsupported Protocol"); + break; + case enuOmniLinkCommStatus.ClientSessionTerminated: + log.Error("CONNECTION STATUS: Client Session Terminated"); + break; + case enuOmniLinkCommStatus.ControllerSessionTerminated: + log.Error("CONNECTION STATUS: Controller Session Terminated"); + break; + case enuOmniLinkCommStatus.CannotStartNewSession: + log.Error("CONNECTION STATUS: Cannot Start New Session"); + break; + case enuOmniLinkCommStatus.LoginFailed: + log.Error("CONNECTION STATUS: Login Failed"); + break; + case enuOmniLinkCommStatus.UnableToOpenSocket: + log.Error("CONNECTION STATUS: Unable To Open Socket"); + break; + case enuOmniLinkCommStatus.UnableToConnect: + log.Error("CONNECTION STATUS: Unable To Connect"); + break; + case enuOmniLinkCommStatus.SocketClosed: + log.Error("CONNECTION STATUS: Socket Closed"); + break; + case enuOmniLinkCommStatus.UnexpectedError: + log.Error("CONNECTION STATUS: Unexpected Error"); + break; + case enuOmniLinkCommStatus.UnableToCreateSocket: + log.Error("CONNECTION STATUS: Unable To Create Socket"); + break; + case enuOmniLinkCommStatus.Retrying: + log.Warn("CONNECTION STATUS: Retrying"); + break; + case enuOmniLinkCommStatus.Connected: + IdentifyController(); + break; + case enuOmniLinkCommStatus.Connecting: + log.Info("CONNECTION STATUS: Connecting"); + break; + case enuOmniLinkCommStatus.Disconnected: + log.Info("CONNECTION STATUS: Disconnected"); + break; + case enuOmniLinkCommStatus.InterruptedFunctionCall: + if (Global.running) + log.Error("CONNECTION STATUS: Interrupted Function Call"); + break; + case enuOmniLinkCommStatus.PermissionDenied: + log.Error("CONNECTION STATUS: Permission Denied"); + break; + case enuOmniLinkCommStatus.BadAddress: + log.Error("CONNECTION STATUS: Bad Address"); + break; + case enuOmniLinkCommStatus.InvalidArgument: + log.Error("CONNECTION STATUS: Invalid Argument"); + break; + case enuOmniLinkCommStatus.TooManyOpenFiles: + log.Error("CONNECTION STATUS: Too Many Open Files"); + break; + case enuOmniLinkCommStatus.ResourceTemporarilyUnavailable: + log.Error("CONNECTION STATUS: Resource Temporarily Unavailable"); + break; + case enuOmniLinkCommStatus.OperationNowInProgress: + log.Warn("CONNECTION STATUS: Operation Now In Progress"); + break; + case enuOmniLinkCommStatus.OperationAlreadyInProgress: + log.Warn("CONNECTION STATUS: Operation Already In Progress"); + break; + case enuOmniLinkCommStatus.SocketOperationOnNonSocket: + log.Error("CONNECTION STATUS: Socket Operation On Non Socket"); + break; + case enuOmniLinkCommStatus.DestinationAddressRequired: + log.Error("CONNECTION STATUS: Destination Address Required"); + break; + case enuOmniLinkCommStatus.MessgeTooLong: + log.Error("CONNECTION STATUS: Message Too Long"); + break; + case enuOmniLinkCommStatus.WrongProtocolType: + log.Error("CONNECTION STATUS: Wrong Protocol Type"); + break; + case enuOmniLinkCommStatus.BadProtocolOption: + log.Error("CONNECTION STATUS: Bad Protocol Option"); + break; + case enuOmniLinkCommStatus.ProtocolNotSupported: + log.Error("CONNECTION STATUS: Protocol Not Supported"); + break; + case enuOmniLinkCommStatus.SocketTypeNotSupported: + log.Error("CONNECTION STATUS: Socket Type Not Supported"); + break; + case enuOmniLinkCommStatus.OperationNotSupported: + log.Error("CONNECTION STATUS: Operation Not Supported"); + break; + case enuOmniLinkCommStatus.ProtocolFamilyNotSupported: + log.Error("CONNECTION STATUS: Protocol Family Not Supported"); + break; + case enuOmniLinkCommStatus.AddressFamilyNotSupported: + log.Error("CONNECTION STATUS: Address Family Not Supported"); + break; + case enuOmniLinkCommStatus.AddressInUse: + log.Error("CONNECTION STATUS: Address In Use"); + break; + case enuOmniLinkCommStatus.AddressNotAvailable: + log.Error("CONNECTION STATUS: Address Not Available"); + break; + case enuOmniLinkCommStatus.NetworkIsDown: + log.Error("CONNECTION STATUS: Network Is Down"); + break; + case enuOmniLinkCommStatus.NetworkIsUnreachable: + log.Error("CONNECTION STATUS: Network Is Unreachable"); + break; + case enuOmniLinkCommStatus.NetworkReset: + log.Error("CONNECTION STATUS: Network Reset"); + break; + case enuOmniLinkCommStatus.ConnectionAborted: + log.Error("CONNECTION STATUS: Connection Aborted"); + break; + case enuOmniLinkCommStatus.ConnectionResetByPeer: + log.Error("CONNECTION STATUS: Connection Reset By Peer"); + break; + case enuOmniLinkCommStatus.NoBufferSpaceAvailable: + log.Error("CONNECTION STATUS: No Buffer Space Available"); + break; + case enuOmniLinkCommStatus.AlreadyConnected: + log.Warn("CONNECTION STATUS: Already Connected"); + break; + case enuOmniLinkCommStatus.NotConnected: + log.Error("CONNECTION STATUS: Not Connected"); + break; + case enuOmniLinkCommStatus.CannotSendAfterShutdown: + log.Error("CONNECTION STATUS: Cannot Send After Shutdown"); + break; + case enuOmniLinkCommStatus.ConnectionTimedOut: + log.Error("CONNECTION STATUS: Connection Timed Out"); + break; + case enuOmniLinkCommStatus.ConnectionRefused: + log.Error("CONNECTION STATUS: Connection Refused"); + break; + case enuOmniLinkCommStatus.HostIsDown: + log.Error("CONNECTION STATUS: Host Is Down"); + break; + case enuOmniLinkCommStatus.HostUnreachable: + log.Error("CONNECTION STATUS: Host Unreachable"); + break; + case enuOmniLinkCommStatus.TooManyProcesses: + log.Error("CONNECTION STATUS: Too Many Processes"); + break; + case enuOmniLinkCommStatus.NetworkSubsystemIsUnavailable: + log.Error("CONNECTION STATUS: Network Subsystem Is Unavailable"); + break; + case enuOmniLinkCommStatus.UnsupportedVersion: + log.Error("CONNECTION STATUS: Unsupported Version"); + break; + case enuOmniLinkCommStatus.NotInitialized: + log.Error("CONNECTION STATUS: Not Initialized"); + break; + case enuOmniLinkCommStatus.ShutdownInProgress: + log.Error("CONNECTION STATUS: Shutdown In Progress"); + break; + case enuOmniLinkCommStatus.ClassTypeNotFound: + log.Error("CONNECTION STATUS: Class Type Not Found"); + break; + case enuOmniLinkCommStatus.HostNotFound: + log.Error("CONNECTION STATUS: Host Not Found"); + break; + case enuOmniLinkCommStatus.HostNotFoundTryAgain: + log.Error("CONNECTION STATUS: Host Not Found Try Again"); + break; + case enuOmniLinkCommStatus.NonRecoverableError: + log.Error("CONNECTION STATUS: Non Recoverable Error"); + break; + case enuOmniLinkCommStatus.NoDataOfRequestedType: + log.Error("CONNECTION STATUS: No Data Of Requested Type"); + break; + default: + break; + } + } + + private void IdentifyController() + { + if (Controller.Connection.ConnectionState == enuOmniLinkConnectionState.Online || + Controller.Connection.ConnectionState == enuOmniLinkConnectionState.OnlineSecure) + { + Controller.Connection.Send(new clsOL2MsgRequestSystemInformation(Controller.Connection), HandleIdentifyController); + } + } + + private void HandleIdentifyController(clsOmniLinkMessageQueueItem M, byte[] B, bool Timeout) + { + if (Timeout) + return; + + if ((B.Length > 3) && (B[2] == (byte)enuOmniLink2MessageType.SystemInformation)) + { + clsOL2MsgSystemInformation MSG = new clsOL2MsgSystemInformation(Controller.Connection, B); + + foreach (enuModel enu in Enum.GetValues(typeof(enuModel))) + { + if (enu == MSG.ModelNumber) + { + Controller.Model = enu; + break; + } + } + + if (Controller.Model == MSG.ModelNumber) + { + Controller.CopySystemInformation(MSG); + log.Info("CONTROLLER IS: " + Controller.GetModelText() + " (" + Controller.GetVersionText() + ")"); + + _ = Connected(); + + return; + } + + log.Error("Model does not match file"); + Controller.Connection.Disconnect(); + } + } + + private async Task Connected() + { + retry = DateTime.MinValue; + + await GetNamedProperties(); + UnsolicitedNotifications(true); + + tstat_timer.Interval = ThermostatTimerInterval(); + tstat_timer.Start(); + + OnConnect?.Invoke(this, new EventArgs()); + } + #endregion + + #region Names + private async Task GetNamedProperties() + { + log.Debug("Retrieving named units"); + + await GetNamed(enuObjectType.Area); + await GetNamed(enuObjectType.Zone); + await GetNamed(enuObjectType.Thermostat); + await GetNamed(enuObjectType.Unit); + await GetNamed(enuObjectType.Message); + await GetNamed(enuObjectType.Button); + } + + private async Task GetNamed(enuObjectType type) + { + GetNextNamed(type, 0); + + await Task.Run(() => + { + log.Debug("Waiting for named units " + type.ToString()); + nameWait.WaitOne(new TimeSpan(0, 0, 10)); + }); + } + + 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 + Controller.Connection.Send(MSG, HandleNamedPropertiesResponse); + } + + private void HandleNamedPropertiesResponse(clsOmniLinkMessageQueueItem M, byte[] B, bool Timeout) + { + if (Timeout) + return; + + // does it look like a valid response + if ((B.Length > 3) && (B[0] == 0x21)) + { + switch ((enuOmniLink2MessageType)B[2]) + { + case enuOmniLink2MessageType.EOD: + nameWait.Set(); + break; + case enuOmniLink2MessageType.Properties: + + clsOL2MsgProperties MSG = new clsOL2MsgProperties(Controller.Connection, B); + + switch (MSG.ObjectType) + { + case enuObjectType.Area: + Controller.Areas.CopyProperties(MSG); + break; + case enuObjectType.Zone: + Controller.Zones.CopyProperties(MSG); + + if (Controller.Zones[MSG.ObjectNumber].IsTemperatureZone()) + Controller.Connection.Send(new clsOL2MsgRequestExtendedStatus(Controller.Connection, enuObjectType.Auxillary, MSG.ObjectNumber, MSG.ObjectNumber), HandleRequestAuxillaryStatus); + + break; + case enuObjectType.Thermostat: + Controller.Thermostats.CopyProperties(MSG); + + if (!tstats.ContainsKey(MSG.ObjectNumber)) + tstats.Add(MSG.ObjectNumber, DateTime.MinValue); + else + tstats[MSG.ObjectNumber] = DateTime.MinValue; + + Controller.Connection.Send(new clsOL2MsgRequestExtendedStatus(Controller.Connection, enuObjectType.Thermostat, MSG.ObjectNumber, MSG.ObjectNumber), HandleRequestThermostatStatus); + log.Debug("Added thermostat to watch list " + Controller.Thermostats[MSG.ObjectNumber].Name); + break; + case enuObjectType.Unit: + Controller.Units.CopyProperties(MSG); + break; + case enuObjectType.Message: + Controller.Messages.CopyProperties(MSG); + break; + case enuObjectType.Button: + Controller.Buttons.CopyProperties(MSG); + break; + default: + break; + } + + GetNextNamed(MSG.ObjectType, MSG.ObjectNumber); + break; + default: + break; + } + } + } + + #endregion + + #region Notifications + private void UnsolicitedNotifications(bool enable) + { + log.Info("Unsolicited notifications " + (enable ? "enabled" : "disabled")); + Controller.Connection.Send(new clsOL2EnableNotifications(Controller.Connection, enable), null); + } + + private bool HandleUnsolicitedPackets(byte[] B) + { + if ((B.Length > 3) && (B[0] == 0x21)) + { + bool handled = false; + + switch ((enuOmniLink2MessageType)B[2]) + { + case enuOmniLink2MessageType.ClearNames: + break; + case enuOmniLink2MessageType.DownloadNames: + break; + case enuOmniLink2MessageType.UploadNames: + break; + case enuOmniLink2MessageType.NameData: + break; + case enuOmniLink2MessageType.ClearVoices: + break; + case enuOmniLink2MessageType.DownloadVoices: + break; + case enuOmniLink2MessageType.UploadVoices: + break; + case enuOmniLink2MessageType.VoiceData: + break; + case enuOmniLink2MessageType.Command: + break; + case enuOmniLink2MessageType.EnableNotifications: + break; + case enuOmniLink2MessageType.SystemInformation: + break; + case enuOmniLink2MessageType.SystemStatus: + break; + case enuOmniLink2MessageType.SystemTroubles: + break; + case enuOmniLink2MessageType.SystemFeatures: + break; + case enuOmniLink2MessageType.Capacities: + break; + case enuOmniLink2MessageType.Properties: + break; + case enuOmniLink2MessageType.Status: + break; + case enuOmniLink2MessageType.EventLogItem: + break; + case enuOmniLink2MessageType.ValidateCode: + break; + case enuOmniLink2MessageType.SystemFormats: + break; + case enuOmniLink2MessageType.Login: + break; + case enuOmniLink2MessageType.Logout: + break; + case enuOmniLink2MessageType.ActivateKeypadEmg: + break; + case enuOmniLink2MessageType.ExtSecurityStatus: + break; + case enuOmniLink2MessageType.CmdExtSecurity: + break; + case enuOmniLink2MessageType.AudioSourceStatus: + break; + case enuOmniLink2MessageType.SystemEvents: + HandleUnsolicitedSystemEvent(B); + handled = true; + break; + case enuOmniLink2MessageType.ZoneReadyStatus: + break; + case enuOmniLink2MessageType.ExtendedStatus: + HandleUnsolicitedExtendedStatus(B); + handled = true; + break; + default: + break; + } + + if (Global.verbose_unhandled && !handled) + log.Debug("Unhandled notification: " + ((enuOmniLink2MessageType)B[2]).ToString()); + } + + return true; + } + + private void HandleUnsolicitedSystemEvent(byte[] B) + { + clsOL2SystemEvent MSG = new clsOL2SystemEvent(Controller.Connection, B); + + SystemStatusEventArgs eventargs = new SystemStatusEventArgs(); + + if (MSG.SystemEvent >= 1 && MSG.SystemEvent <= 255) + { + eventargs.Type = enuEventType.USER_MACRO_BUTTON; + eventargs.Value = ((int)MSG.SystemEvent).ToString() + " " + Controller.Buttons[MSG.SystemEvent].Name; + + OnSystemStatus?.Invoke(this, eventargs); + } + else if (MSG.SystemEvent >= 768 && MSG.SystemEvent <= 771) + { + eventargs.Type = enuEventType.PHONE_; + + if (MSG.SystemEvent == 768) + { + eventargs.Value = "DEAD"; + eventargs.SendNotification = true; + } + else if (MSG.SystemEvent == 769) + eventargs.Value = "RING"; + else if (MSG.SystemEvent == 770) + eventargs.Value = "OFF HOOK"; + else if (MSG.SystemEvent == 771) + eventargs.Value = "ON HOOK"; + + OnSystemStatus?.Invoke(this, eventargs); + } + else if (MSG.SystemEvent >= 772 && MSG.SystemEvent <= 773) + { + eventargs.Type = enuEventType.AC_POWER_; + eventargs.SendNotification = true; + + if (MSG.SystemEvent == 772) + eventargs.Value = "OFF"; + else if (MSG.SystemEvent == 773) + eventargs.Value = "RESTORED"; + + OnSystemStatus?.Invoke(this, eventargs); + } + else if (MSG.SystemEvent >= 774 && MSG.SystemEvent <= 775) + { + eventargs.Type = enuEventType.BATTERY_; + eventargs.SendNotification = true; + + if (MSG.SystemEvent == 774) + eventargs.Value = "LOW"; + else if (MSG.SystemEvent == 775) + eventargs.Value = "OK"; + + OnSystemStatus?.Invoke(this, eventargs); + } + else if (MSG.SystemEvent >= 776 && MSG.SystemEvent <= 777) + { + eventargs.Type = enuEventType.DCM_; + eventargs.SendNotification = true; + + if (MSG.SystemEvent == 776) + eventargs.Value = "TROUBLE"; + else if (MSG.SystemEvent == 777) + eventargs.Value = "OK"; + + OnSystemStatus?.Invoke(this, eventargs); + } + else if (MSG.SystemEvent >= 778 && MSG.SystemEvent <= 781) + { + eventargs.Type = enuEventType.ENERGY_COST_; + + if (MSG.SystemEvent == 778) + eventargs.Value = "LOW"; + else if (MSG.SystemEvent == 779) + eventargs.Value = "MID"; + else if (MSG.SystemEvent == 780) + eventargs.Value = "HIGH"; + else if (MSG.SystemEvent == 781) + eventargs.Value = "CRITICAL"; + + OnSystemStatus?.Invoke(this, eventargs); + } + else if (MSG.SystemEvent >= 782 && MSG.SystemEvent <= 787) + { + eventargs.Type = enuEventType.CAMERA; + eventargs.Value = (MSG.SystemEvent - 781).ToString(); + + OnSystemStatus?.Invoke(this, eventargs); + } + else if (MSG.SystemEvent >= 61440 && MSG.SystemEvent <= 64511) + { + eventargs.Type = enuEventType.SWITCH_PRESS; + int state = (int)MSG.Data[1] - 240; + int id = (int)MSG.Data[2]; + + eventargs.Value = "Unit: " + id + ", State: " + state; + + OnSystemStatus?.Invoke(this, eventargs); + } + else if (MSG.SystemEvent >= 64512 && MSG.SystemEvent <= 65535) + { + eventargs.Type = enuEventType.UPB_LINK; + int state = (int)MSG.Data[1] - 252; + int id = (int)MSG.Data[2]; + + eventargs.Value = "Link: " + id + ", State: " + state; + + OnSystemStatus?.Invoke(this, eventargs); + } + else if (Global.verbose_unhandled) + { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < MSG.MessageLength; i++) + sb.Append(MSG.Data[i].ToString() + " "); + log.Debug("Unhandled SystemEvent Raw: " + sb.ToString() + "Num: " + MSG.SystemEvent); + + int num = ((int)MSG.MessageLength - 1) / 2; + for (int i = 0; i < num; i++) + { + log.Debug("Unhandled SystemEvent: " + + (int)MSG.Data[(i * 2) + 1] + " " + (int)MSG.Data[(i * 2) + 2] + ": " + + Convert.ToString(MSG.Data[(i * 2) + 1], 2).PadLeft(8, '0') + " " + Convert.ToString(MSG.Data[(i * 2) + 2], 2).PadLeft(8, '0')); + } + } + } + + private void HandleUnsolicitedExtendedStatus(byte[] B) + { + clsOL2MsgExtendedStatus MSG = new clsOL2MsgExtendedStatus(Controller.Connection, B); + + switch (MSG.ObjectType) + { + case enuObjectType.Area: + for (byte i = 0; i < MSG.AreaCount(); i++) + { + Controller.Areas[MSG.ObjectNumber(i)].CopyExtendedStatus(MSG, i); + OnAreaStatus?.Invoke(this, new AreaStatusEventArgs() + { + ID = MSG.ObjectNumber(i), + Area = Controller.Areas[MSG.ObjectNumber(i)] + }); + } + break; + case enuObjectType.Auxillary: + for (byte i = 0; i < MSG.AuxStatusCount(); i++) + { + Controller.Zones[MSG.ObjectNumber(i)].CopyAuxExtendedStatus(MSG, i); + OnZoneStatus?.Invoke(this, new ZoneStatusEventArgs() + { + ID = MSG.ObjectNumber(i), + Zone = Controller.Zones[MSG.ObjectNumber(i)] + }); + } + break; + case enuObjectType.Zone: + for (byte i = 0; i < MSG.ZoneStatusCount(); i++) + { + Controller.Zones[MSG.ObjectNumber(i)].CopyExtendedStatus(MSG, i); + OnZoneStatus?.Invoke(this, new ZoneStatusEventArgs() + { + ID = MSG.ObjectNumber(i), + Zone = Controller.Zones[MSG.ObjectNumber(i)] + }); + } + break; + case enuObjectType.Thermostat: + for (byte i = 0; i < MSG.ThermostatStatusCount(); i++) + { + lock (tstat_lock) + { + Controller.Thermostats[MSG.ObjectNumber(i)].CopyExtendedStatus(MSG, i); + OnThermostatStatus?.Invoke(this, new ThermostatStatusEventArgs() + { + ID = MSG.ObjectNumber(i), + Thermostat = Controller.Thermostats[MSG.ObjectNumber(i)], + EventTimer = false + }); + + if (!tstats.ContainsKey(MSG.ObjectNumber(i))) + tstats.Add(MSG.ObjectNumber(i), DateTime.Now); + else + tstats[MSG.ObjectNumber(i)] = DateTime.Now; + + if (Global.verbose_thermostat_timer) + log.Debug("Unsolicited status received for Thermostat " + Controller.Thermostats[MSG.ObjectNumber(i)].Name); + } + } + break; + case enuObjectType.Unit: + for (byte i = 0; i < MSG.UnitStatusCount(); i++) + { + Controller.Units[MSG.ObjectNumber(i)].CopyExtendedStatus(MSG, i); + OnUnitStatus?.Invoke(this, new UnitStatusEventArgs() + { + ID = MSG.ObjectNumber(i), + Unit = Controller.Units[MSG.ObjectNumber(i)] + }); + } + break; + case enuObjectType.Message: + for (byte i = 0; i < MSG.MessageCount(); i++) + { + Controller.Messages[MSG.ObjectNumber(i)].CopyExtendedStatus(MSG, i); + OnMessageStatus?.Invoke(this, new MessageStatusEventArgs() + { + ID = MSG.ObjectNumber(i), + Message = Controller.Messages[MSG.ObjectNumber(i)] + }); + } + break; + default: + if (Global.verbose_unhandled) + { + StringBuilder sb = new StringBuilder(); + foreach (byte b in MSG.ToByteArray()) + sb.Append(b.ToString() + " "); + + log.Debug("Unhandled ExtendedStatus" + MSG.ObjectType.ToString() + " " + sb.ToString()); + } + break; + } + } + #endregion + + #region Thermostats + static double ThermostatTimerInterval() + { + DateTime now = DateTime.Now; + return ((60 - now.Second) * 1000 - now.Millisecond) + new TimeSpan(0, 0, 30).TotalMilliseconds; + } + + private static DateTime RoundToMinute(DateTime dt) + { + return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, 0); + } + + private void tstat_timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) + { + lock (tstat_lock) + { + foreach (KeyValuePair tstat in tstats) + { + // Poll every 4 minutes if no prior update + if (RoundToMinute(tstat.Value).AddMinutes(4) <= RoundToMinute(DateTime.Now)) + { + 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 " + Controller.Thermostats[tstat.Key].Name); + } + + // Log every minute if update within 5 minutes and connected + if (RoundToMinute(tstat.Value).AddMinutes(5) > RoundToMinute(DateTime.Now) && + (Controller.Connection.ConnectionState == enuOmniLinkConnectionState.Online || + Controller.Connection.ConnectionState == enuOmniLinkConnectionState.OnlineSecure)) + { + if (Controller.Thermostats[tstat.Key].Temp > 0) + { + OnThermostatStatus?.Invoke(this, new ThermostatStatusEventArgs() + { + ID = tstat.Key, + Thermostat = Controller.Thermostats[tstat.Key], + EventTimer = true + }); + } + else if (Global.verbose_thermostat_timer) + log.Warn("Not logging 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); + } + } + + tstat_timer.Interval = ThermostatTimerInterval(); + tstat_timer.Start(); + } + + private void HandleRequestThermostatStatus(clsOmniLinkMessageQueueItem M, byte[] B, bool Timeout) + { + if (Timeout) + return; + + clsOL2MsgExtendedStatus MSG = new clsOL2MsgExtendedStatus(Controller.Connection, B); + + for (byte i = 0; i < MSG.ThermostatStatusCount(); i++) + { + lock (tstat_lock) + { + Controller.Thermostats[MSG.ObjectNumber(i)].CopyExtendedStatus(MSG, i); + + if (!tstats.ContainsKey(MSG.ObjectNumber(i))) + tstats.Add(MSG.ObjectNumber(i), DateTime.Now); + else + tstats[MSG.ObjectNumber(i)] = DateTime.Now; + + if (Global.verbose_thermostat_timer) + log.Debug("Polling status received for Thermostat " + Controller.Thermostats[MSG.ObjectNumber(i)].Name); + } + } + } + #endregion + + #region Auxiliary + private void HandleRequestAuxillaryStatus(clsOmniLinkMessageQueueItem M, byte[] B, bool Timeout) + { + if (Timeout) + return; + + clsOL2MsgExtendedStatus MSG = new clsOL2MsgExtendedStatus(Controller.Connection, B); + + for (byte i = 0; i < MSG.AuxStatusCount(); i++) + { + Controller.Zones[MSG.ObjectNumber(i)].CopyAuxExtendedStatus(MSG, i); + } + } + #endregion + } +} diff --git a/OmniLinkBridge/Modules/TimeSyncModule.cs b/OmniLinkBridge/Modules/TimeSyncModule.cs new file mode 100644 index 0000000..bf61ee5 --- /dev/null +++ b/OmniLinkBridge/Modules/TimeSyncModule.cs @@ -0,0 +1,111 @@ +using HAI_Shared; +using OmniLinkBridge.OmniLink; +using log4net; +using System; +using System.Reflection; +using System.Threading; + +namespace OmniLinkBridge.Modules +{ + public class TimeSyncModule : IModule + { + private static ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + private OmniLinkII OmniLink { get; set; } + + private System.Timers.Timer tsync_timer = new System.Timers.Timer(); + private DateTime tsync_check = DateTime.MinValue; + + private readonly AutoResetEvent trigger = new AutoResetEvent(false); + + public TimeSyncModule(OmniLinkII omni) + { + OmniLink = omni; + } + + public void Startup() + { + tsync_timer.Elapsed += tsync_timer_Elapsed; + tsync_timer.AutoReset = false; + + tsync_check = DateTime.MinValue; + + tsync_timer.Interval = TimeTimerInterval(); + tsync_timer.Start(); + + // Wait until shutdown + trigger.WaitOne(); + + tsync_timer.Stop(); + } + + public void Shutdown() + { + trigger.Set(); + } + + static double TimeTimerInterval() + { + DateTime now = DateTime.Now; + return ((60 - now.Second) * 1000 - now.Millisecond); + } + + private void tsync_timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) + { + if (tsync_check.AddMinutes(Global.time_interval) < DateTime.Now) + OmniLink.Controller.Connection.Send(new clsOL2MsgRequestSystemStatus(OmniLink.Controller.Connection), HandleRequestSystemStatus); + + tsync_timer.Interval = TimeTimerInterval(); + tsync_timer.Start(); + } + + private void HandleRequestSystemStatus(clsOmniLinkMessageQueueItem M, byte[] B, bool Timeout) + { + if (Timeout) + return; + + tsync_check = DateTime.Now; + + clsOL2MsgSystemStatus MSG = new clsOL2MsgSystemStatus(OmniLink.Controller.Connection, B); + + DateTime time; + try + { + // The controller uses 2 digit years and C# uses 4 digit years + // 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); + } + catch + { + log.Warn("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, + (byte)now.Hour, (byte)now.Minute, (byte)(now.IsDaylightSavingTime() ? 1 : 0)), HandleSetTime); + + return; + } + + double adj = (DateTime.Now - time).Duration().TotalSeconds; + + if (adj > Global.time_drift) + { + log.Warn("Controller time " + time.ToString("MM/dd/yyyy HH:mm:ss") + " out of sync by " + adj + " seconds"); + + 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, + (byte)now.Hour, (byte)now.Minute, (byte)(now.IsDaylightSavingTime() ? 1 : 0)), HandleSetTime); + } + } + + private void HandleSetTime(clsOmniLinkMessageQueueItem M, byte[] B, bool Timeout) + { + if (Timeout) + return; + + log.Debug("Controller time has been successfully set"); + } + } +} diff --git a/OmniLinkBridge/Modules/WebServiceModule.cs b/OmniLinkBridge/Modules/WebServiceModule.cs new file mode 100644 index 0000000..52f9ab9 --- /dev/null +++ b/OmniLinkBridge/Modules/WebServiceModule.cs @@ -0,0 +1,119 @@ +using HAI_Shared; +using OmniLinkBridge.Modules; +using OmniLinkBridge.OmniLink; +using OmniLinkBridge.WebAPI; +using log4net; +using Newtonsoft.Json; +using System; +using System.Reflection; +using System.ServiceModel; +using System.ServiceModel.Description; +using System.ServiceModel.Web; +using System.Threading; + +namespace OmniLinkBridge +{ + public class WebServiceModule : IModule + { + private static ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + public static OmniLinkII OmniLink { get; private set; } + + private WebServiceHost host; + + private readonly AutoResetEvent trigger = new AutoResetEvent(false); + + public WebServiceModule(OmniLinkII omni) + { + OmniLink = omni; + OmniLink.OnAreaStatus += Omnilink_OnAreaStatus; + OmniLink.OnZoneStatus += Omnilink_OnZoneStatus; + OmniLink.OnUnitStatus += Omnilink_OnUnitStatus; + OmniLink.OnThermostatStatus += Omnilink_OnThermostatStatus; + } + + public void Startup() + { + WebNotification.RestoreSubscriptions(); + + Uri uri = new Uri("http://0.0.0.0:" + Global.webapi_port + "/"); + host = new WebServiceHost(typeof(OmniLinkService), uri); + + try + { + ServiceEndpoint ep = host.AddServiceEndpoint(typeof(IOmniLinkService), new WebHttpBinding(), ""); + host.Open(); + + log.Info("Listening on " + uri.ToString()); + } + catch (CommunicationException ex) + { + log.Error("An exception occurred starting web service", ex); + host.Abort(); + } + + // Wait until shutdown + trigger.WaitOne(); + + if (host != null) + host.Close(); + + WebNotification.SaveSubscriptions(); + } + + public void Shutdown() + { + trigger.Set(); + } + + private void Omnilink_OnAreaStatus(object sender, AreaStatusEventArgs e) + { + WebNotification.Send("area", JsonConvert.SerializeObject(e.Area.ToContract())); + } + + private void Omnilink_OnZoneStatus(object sender, ZoneStatusEventArgs e) + { + if (e.Zone.IsTemperatureZone()) + { + WebNotification.Send("temp", JsonConvert.SerializeObject(e.Zone.ToContract())); + return; + } + + switch (e.Zone.ZoneType) + { + case enuZoneType.EntryExit: + case enuZoneType.X2EntryDelay: + case enuZoneType.X4EntryDelay: + case enuZoneType.Perimeter: + case enuZoneType.Tamper: + case enuZoneType.Auxiliary: + WebNotification.Send("contact", JsonConvert.SerializeObject(e.Zone.ToContract())); + break; + case enuZoneType.AwayInt: + case enuZoneType.NightInt: + WebNotification.Send("motion", JsonConvert.SerializeObject(e.Zone.ToContract())); + break; + case enuZoneType.Water: + WebNotification.Send("water", JsonConvert.SerializeObject(e.Zone.ToContract())); + break; + case enuZoneType.Fire: + WebNotification.Send("smoke", JsonConvert.SerializeObject(e.Zone.ToContract())); + break; + case enuZoneType.Gas: + WebNotification.Send("co", JsonConvert.SerializeObject(e.Zone.ToContract())); + break; + } + } + + private void Omnilink_OnUnitStatus(object sender, UnitStatusEventArgs e) + { + WebNotification.Send("unit", JsonConvert.SerializeObject(e.Unit.ToContract())); + } + + private void Omnilink_OnThermostatStatus(object sender, ThermostatStatusEventArgs e) + { + if(!e.EventTimer) + WebNotification.Send("thermostat", JsonConvert.SerializeObject(e.Thermostat.ToContract())); + } + } +} diff --git a/OmniLinkBridge/Notifications/EmailNotification.cs b/OmniLinkBridge/Notifications/EmailNotification.cs new file mode 100644 index 0000000..3f4b47f --- /dev/null +++ b/OmniLinkBridge/Notifications/EmailNotification.cs @@ -0,0 +1,43 @@ +using log4net; +using System; +using System.Net; +using System.Net.Mail; +using System.Reflection; + +namespace OmniLinkBridge.Notifications +{ + public class EmailNotification : INotification + { + private static ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + public void Notify(string source, string description, NotificationPriority priority) + { + foreach (MailAddress address in Global.mail_to) + { + MailMessage mail = new MailMessage(); + mail.From = Global.mail_from; + mail.To.Add(address); + mail.Subject = "OmniLinkBridge - " + source; + mail.Body = source + ": " + description; + + using (SmtpClient smtp = new SmtpClient(Global.mail_server, Global.mail_port)) + { + if (!string.IsNullOrEmpty(Global.mail_username)) + { + smtp.UseDefaultCredentials = false; + smtp.Credentials = new NetworkCredential(Global.mail_username, Global.mail_password); + } + + try + { + smtp.Send(mail); + } + catch (Exception ex) + { + log.Error("An error occurred sending notification", ex); + } + } + } + } + } +} diff --git a/OmniLinkBridge/Notifications/INotification.cs b/OmniLinkBridge/Notifications/INotification.cs new file mode 100644 index 0000000..64da673 --- /dev/null +++ b/OmniLinkBridge/Notifications/INotification.cs @@ -0,0 +1,7 @@ +namespace OmniLinkBridge.Notifications +{ + public interface INotification + { + void Notify(string source, string description, NotificationPriority priority); + } +} diff --git a/OmniLinkBridge/Notifications/Notification.cs b/OmniLinkBridge/Notifications/Notification.cs new file mode 100644 index 0000000..2ae1b23 --- /dev/null +++ b/OmniLinkBridge/Notifications/Notification.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace OmniLinkBridge.Notifications +{ + public static class Notification + { + private static readonly List providers = new List() + { + new EmailNotification(), + new ProwlNotification(), + new PushoverNotification() + }; + + public static void Notify(string source, string description, NotificationPriority priority = NotificationPriority.Normal) + { + Parallel.ForEach(providers, (provider) => + { + provider.Notify(source, description, priority); + }); + } + } +} diff --git a/OmniLinkBridge/Notifications/NotificationPriority.cs b/OmniLinkBridge/Notifications/NotificationPriority.cs new file mode 100644 index 0000000..0b5bf90 --- /dev/null +++ b/OmniLinkBridge/Notifications/NotificationPriority.cs @@ -0,0 +1,30 @@ +namespace OmniLinkBridge.Notifications +{ + public enum NotificationPriority + { + /// + /// Generate no notification/alert + /// + VeryLow = -2, + + /// + /// Always send as a quiet notification + /// + Moderate = -1, + + /// + /// Trigger sound, vibration, and display an alert according to the user's device settings + /// + Normal = 0, + + /// + /// Display as high-priority and bypass the user's quiet hours + /// + High = 1, + + /// + /// Require confirmation from the user + /// + Emergency = 2, + }; +} diff --git a/OmniLinkBridge/Notifications/ProwlNotification.cs b/OmniLinkBridge/Notifications/ProwlNotification.cs new file mode 100644 index 0000000..d34cf50 --- /dev/null +++ b/OmniLinkBridge/Notifications/ProwlNotification.cs @@ -0,0 +1,42 @@ +using log4net; +using System; +using System.Collections.Generic; +using System.Net; +using System.Reflection; + +namespace OmniLinkBridge.Notifications +{ + public class ProwlNotification : INotification + { + private static ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + private static Uri URI = new Uri("https://api.prowlapp.com/publicapi/add"); + + public void Notify(string source, string description, NotificationPriority priority) + { + foreach (string key in Global.prowl_key) + { + List parameters = new List(); + + parameters.Add("apikey=" + key); + parameters.Add("priority= " + (int)priority); + parameters.Add("application=OmniLinkBridge"); + parameters.Add("event=" + source); + parameters.Add("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; + } + } + } + + private void client_UploadStringCompleted(object sender, UploadStringCompletedEventArgs e) + { + if (e.Error != null) + log.Error("An error occurred sending notification", e.Error); + } + } +} diff --git a/OmniLinkBridge/Notifications/PushoverNotification.cs b/OmniLinkBridge/Notifications/PushoverNotification.cs new file mode 100644 index 0000000..a360759 --- /dev/null +++ b/OmniLinkBridge/Notifications/PushoverNotification.cs @@ -0,0 +1,41 @@ +using log4net; +using System; +using System.Collections.Specialized; +using System.Net; +using System.Reflection; + +namespace OmniLinkBridge.Notifications +{ + public class PushoverNotification : INotification + { + private static ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + private static Uri URI = new Uri("https://api.pushover.net/1/messages.json"); + + public void Notify(string source, string description, NotificationPriority priority) + { + foreach (string key in Global.pushover_user) + { + var parameters = new NameValueCollection { + { "token", Global.pushover_token }, + { "user", key }, + { "priority", ((int)priority).ToString() }, + { "title", source }, + { "message", description } + }; + + using (WebClient client = new WebClient()) + { + client.UploadValues(URI, parameters); + client.UploadStringCompleted += client_UploadStringCompleted; + } + } + } + + private void client_UploadStringCompleted(object sender, UploadStringCompletedEventArgs e) + { + if (e.Error != null) + log.Error("An error occurred sending notification", e.Error); + } + } +} diff --git a/OmniLinkBridge/OmniLink/AreaStatusEventArgs.cs b/OmniLinkBridge/OmniLink/AreaStatusEventArgs.cs new file mode 100644 index 0000000..9f2a333 --- /dev/null +++ b/OmniLinkBridge/OmniLink/AreaStatusEventArgs.cs @@ -0,0 +1,11 @@ +using HAI_Shared; +using System; + +namespace OmniLinkBridge.OmniLink +{ + public class AreaStatusEventArgs : EventArgs + { + public ushort ID { get; set; } + public clsArea Area { get; set; } + } +} diff --git a/OmniLinkBridge/OmniLink/MessageStatusEventArgs.cs b/OmniLinkBridge/OmniLink/MessageStatusEventArgs.cs new file mode 100644 index 0000000..a63cfca --- /dev/null +++ b/OmniLinkBridge/OmniLink/MessageStatusEventArgs.cs @@ -0,0 +1,11 @@ +using HAI_Shared; +using System; + +namespace OmniLinkBridge.OmniLink +{ + public class MessageStatusEventArgs : EventArgs + { + public ushort ID { get; set; } + public clsMessage Message { get; set; } + } +} diff --git a/OmniLinkBridge/OmniLink/SystemStatusEventArgs.cs b/OmniLinkBridge/OmniLink/SystemStatusEventArgs.cs new file mode 100644 index 0000000..5a62f5e --- /dev/null +++ b/OmniLinkBridge/OmniLink/SystemStatusEventArgs.cs @@ -0,0 +1,12 @@ +using HAI_Shared; +using System; + +namespace OmniLinkBridge.OmniLink +{ + public class SystemStatusEventArgs : EventArgs + { + public enuEventType Type { get; set; } + public string Value { get; set; } + public bool SendNotification { get; set; } + } +} diff --git a/OmniLinkBridge/OmniLink/ThermostatStatusEventArgs.cs b/OmniLinkBridge/OmniLink/ThermostatStatusEventArgs.cs new file mode 100644 index 0000000..5372e22 --- /dev/null +++ b/OmniLinkBridge/OmniLink/ThermostatStatusEventArgs.cs @@ -0,0 +1,12 @@ +using HAI_Shared; +using System; + +namespace OmniLinkBridge.OmniLink +{ + public class ThermostatStatusEventArgs : EventArgs + { + public ushort ID { get; set; } + public clsThermostat Thermostat { get; set; } + public bool EventTimer { get; set; } + } +} diff --git a/OmniLinkBridge/OmniLink/UnitStatusEventArgs.cs b/OmniLinkBridge/OmniLink/UnitStatusEventArgs.cs new file mode 100644 index 0000000..14c9b4d --- /dev/null +++ b/OmniLinkBridge/OmniLink/UnitStatusEventArgs.cs @@ -0,0 +1,11 @@ +using HAI_Shared; +using System; + +namespace OmniLinkBridge.OmniLink +{ + public class UnitStatusEventArgs : EventArgs + { + public ushort ID { get; set; } + public clsUnit Unit { get; set; } + } +} diff --git a/OmniLinkBridge/OmniLink/ZoneStatusEventArgs.cs b/OmniLinkBridge/OmniLink/ZoneStatusEventArgs.cs new file mode 100644 index 0000000..f0a5885 --- /dev/null +++ b/OmniLinkBridge/OmniLink/ZoneStatusEventArgs.cs @@ -0,0 +1,11 @@ +using HAI_Shared; +using System; + +namespace OmniLinkBridge.OmniLink +{ + public class ZoneStatusEventArgs : EventArgs + { + public ushort ID { get; set; } + public clsZone Zone { get; set; } + } +} diff --git a/OmniLinkBridge/OmniLinkBridge.csproj b/OmniLinkBridge/OmniLinkBridge.csproj new file mode 100644 index 0000000..0e4c77f --- /dev/null +++ b/OmniLinkBridge/OmniLinkBridge.csproj @@ -0,0 +1,170 @@ + + + + Debug + x86 + 8.0.30703 + 2.0 + {0A636707-98BA-45AB-9843-AED430933CEE} + Exe + Properties + OmniLinkBridge + OmniLinkBridge + v4.5.2 + 512 + + + + x86 + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + x86 + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + true + bin\Debug\ + DEBUG;TRACE + full + AnyCPU + prompt + MinimumRecommendedRules.ruleset + + + bin\Release\ + TRACE + true + pdbonly + AnyCPU + prompt + MinimumRecommendedRules.ruleset + + + true + + + OmniLinkBridge.Program + + + + .\HAI.Controller.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Component + + + ProjectInstaller.cs + + + + + Component + + + Service.cs + + + + + + + + + + + + + + + Designer + + + Always + + + + + ProjectInstaller.cs + + + Service.cs + + + + + 2.0.8 + + + 2.8.4 + + + 11.0.2 + + + + + \ No newline at end of file diff --git a/OmniLinkBridge/OmniLinkBridge.ini b/OmniLinkBridge/OmniLinkBridge.ini new file mode 100644 index 0000000..032eb8b --- /dev/null +++ b/OmniLinkBridge/OmniLinkBridge.ini @@ -0,0 +1,72 @@ +# HAI / Leviton Omni Controller +controller_address = +controller_port = 4369 +controller_key1 = 00-00-00-00-00-00-00-00 +controller_key2 = 00-00-00-00-00-00-00-00 + +# Controller Time Sync (yes/no) +# time_check is interval in minutes to check controller time +# time_adj is the drift in seconds to allow before an adjustment is made +time_sync = yes +time_interval = 60 +time_drift = 10 + +# Verbose Console (yes/no) +verbose_unhandled = yes +verbose_area = yes +verbose_zone = yes +verbose_thermostat_timer = yes +verbose_thermostat = yes +verbose_unit = yes +verbose_message = yes + +# mySQL Logging (yes/no) +mysql_logging = no +mysql_connection = + +# Web Service (yes/no) +# Can be used for integration with Samsung SmartThings +webapi_enabled = yes +webapi_port = 8000 + +# MQTT +# Can be used for integration with Home Assistant +mqtt_enabled = no +mqtt_server = +mqtt_port = 1883 +mqtt_username = +mqtt_password = +mqtt_discovery_prefix = homeassistant +# specify a range of numbers like 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 +#mqtt_discovery_override_zone = id=5;device_class=garage_door +#mqtt_discovery_override_zone = id=6;device_class=garage_door + +# Notifications (yes/no) +# Always sent for area alarms and critical system events +# Optionally enable for area status changes and console messages +notify_area = no +notify_message = no + +# Email Notifications +# mail_username and mail_password optional for authenticated mail +mail_server = +mail_port = 25 +mail_username = +mail_password = +mail_from = OmniLinkBridge@localhost +#mail_to = +#mail_to = + +# Prowl Notifications +# Register for API key at http://www.prowlapp.com +#prowl_key = +#prowl_key = + +# Pushover Notifications +# Register for API token at http://www.pushover.net +#pushover_token = +#pushover_user = +#pushover_user = \ No newline at end of file diff --git a/HAILogger/Program.cs b/OmniLinkBridge/Program.cs similarity index 77% rename from HAILogger/Program.cs rename to OmniLinkBridge/Program.cs index acc771f..63226d8 100644 --- a/HAILogger/Program.cs +++ b/OmniLinkBridge/Program.cs @@ -4,7 +4,7 @@ using System.IO; using System.Reflection; using System.ServiceProcess; -namespace HAILogger +namespace OmniLinkBridge { class Program { @@ -26,8 +26,8 @@ namespace HAILogger case "-c": Global.config_file = args[++i]; break; - case "-l": - Global.config_file = args[++i]; + case "-s": + Global.webapi_subscriptions_file = args[++i]; break; case "-i": interactive = true; @@ -35,15 +35,15 @@ namespace HAILogger } } - if (string.IsNullOrEmpty(Global.log_file)) - Global.log_file = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + - Path.DirectorySeparatorChar + "EventLog.txt"; - if (string.IsNullOrEmpty(Global.config_file)) Global.config_file = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + - Path.DirectorySeparatorChar + "HAILogger.ini"; + Path.DirectorySeparatorChar + "OmniLinkBridge.ini"; - Global.event_source = "HAI Logger"; + if (string.IsNullOrEmpty(Global.webapi_subscriptions_file)) + Global.webapi_subscriptions_file = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + + Path.DirectorySeparatorChar + "WebSubscriptions.json"; + + log4net.Config.XmlConfigurator.Configure(); try { @@ -89,9 +89,9 @@ namespace HAILogger static void ShowHelp() { Console.WriteLine( - AppDomain.CurrentDomain.FriendlyName + " [-c config_file] [-l log_file] [-i]\n" + - "\t-c Specifies the name of the config file. Default is HAILogger.ini\n" + - "\t-l Specifies the name of the log file. Default is EventLog.txt\n" + + AppDomain.CurrentDomain.FriendlyName + " [-c config_file] [-s subscriptions_file] [-i]\n" + + "\t-c Specifies the configuration file. Default is OmniLinkBridge.ini\n" + + "\t-s Specifies the web api subscriptions file. Default is WebSubscriptions.json\n" + "\t-i Run in interactive mode"); } } diff --git a/HAILogger/ProjectInstaller.Designer.cs b/OmniLinkBridge/ProjectInstaller.Designer.cs similarity index 94% rename from HAILogger/ProjectInstaller.Designer.cs rename to OmniLinkBridge/ProjectInstaller.Designer.cs index 3d05edb..2a7aa6c 100644 --- a/HAILogger/ProjectInstaller.Designer.cs +++ b/OmniLinkBridge/ProjectInstaller.Designer.cs @@ -1,4 +1,4 @@ -namespace HAILogger +namespace OmniLinkBridge { partial class ProjectInstaller { @@ -38,7 +38,7 @@ // // serviceInstaller // - this.serviceInstaller.ServiceName = "HAILogger"; + this.serviceInstaller.ServiceName = "OmniLinkBridge"; // // ProjectInstaller // diff --git a/HAILogger/ProjectInstaller.cs b/OmniLinkBridge/ProjectInstaller.cs similarity index 93% rename from HAILogger/ProjectInstaller.cs rename to OmniLinkBridge/ProjectInstaller.cs index e950e2b..d4681e6 100644 --- a/HAILogger/ProjectInstaller.cs +++ b/OmniLinkBridge/ProjectInstaller.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Configuration.Install; -namespace HAILogger +namespace OmniLinkBridge { [RunInstaller(true)] public partial class ProjectInstaller : System.Configuration.Install.Installer diff --git a/HAILogger/ProjectInstaller.resx b/OmniLinkBridge/ProjectInstaller.resx similarity index 100% rename from HAILogger/ProjectInstaller.resx rename to OmniLinkBridge/ProjectInstaller.resx diff --git a/HAILogger/Properties/AssemblyInfo.cs b/OmniLinkBridge/Properties/AssemblyInfo.cs similarity index 87% rename from HAILogger/Properties/AssemblyInfo.cs rename to OmniLinkBridge/Properties/AssemblyInfo.cs index 1cad50e..0cfd85f 100644 --- a/HAILogger/Properties/AssemblyInfo.cs +++ b/OmniLinkBridge/Properties/AssemblyInfo.cs @@ -5,12 +5,12 @@ using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("HAILogger")] +[assembly: AssemblyTitle("OmniLinkBridge")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Excalibur Partners, LLC")] -[assembly: AssemblyProduct("HAILogger")] -[assembly: AssemblyCopyright("Copyright © Excalibur Partners, LLC 2016")] +[assembly: AssemblyProduct("OmniLinkBridge")] +[assembly: AssemblyCopyright("Copyright © Excalibur Partners, LLC 2018")] [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.0.8.0")] -[assembly: AssemblyFileVersion("1.0.8.0")] +[assembly: AssemblyVersion("1.1.0.0")] +[assembly: AssemblyFileVersion("1.1.0.0")] diff --git a/HAILogger/Service.Designer.cs b/OmniLinkBridge/Service.Designer.cs similarity index 93% rename from HAILogger/Service.Designer.cs rename to OmniLinkBridge/Service.Designer.cs index ceaf929..699d1bc 100644 --- a/HAILogger/Service.Designer.cs +++ b/OmniLinkBridge/Service.Designer.cs @@ -1,4 +1,4 @@ -namespace HAILogger +namespace OmniLinkBridge { partial class Service { @@ -33,7 +33,7 @@ // this.AutoLog = false; this.CanShutdown = true; - this.ServiceName = "HAILogger"; + this.ServiceName = "OmniLinkBridge"; } diff --git a/HAILogger/Service.cs b/OmniLinkBridge/Service.cs similarity index 96% rename from HAILogger/Service.cs rename to OmniLinkBridge/Service.cs index 2db5a77..9c03570 100644 --- a/HAILogger/Service.cs +++ b/OmniLinkBridge/Service.cs @@ -6,7 +6,7 @@ using System.Diagnostics; using System.ServiceProcess; using System.Text; -namespace HAILogger +namespace OmniLinkBridge { partial class Service : ServiceBase { diff --git a/HAILogger/Service.resx b/OmniLinkBridge/Service.resx similarity index 100% rename from HAILogger/Service.resx rename to OmniLinkBridge/Service.resx diff --git a/HAILogger/Settings.cs b/OmniLinkBridge/Settings.cs similarity index 56% rename from HAILogger/Settings.cs rename to OmniLinkBridge/Settings.cs index 69e7980..2e54e56 100644 --- a/HAILogger/Settings.cs +++ b/OmniLinkBridge/Settings.cs @@ -1,48 +1,37 @@ +using log4net; +using OmniLinkBridge.MQTT; using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Collections.Specialized; using System.IO; +using System.Linq; using System.Net; using System.Net.Mail; +using System.Reflection; -namespace HAILogger +namespace OmniLinkBridge { - static class Settings + public static class Settings { + private static ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + public static void LoadSettings() { NameValueCollection settings = LoadCollection(Global.config_file); - // HAI Controller - Global.hai_address = settings["hai_address"]; - Global.hai_port = ValidatePort(settings, "hai_port"); - Global.hai_key1 = settings["hai_key1"]; - Global.hai_key2 = settings["hai_key2"]; - Global.hai_time_sync = ValidateYesNo(settings, "hai_time_sync"); - Global.hai_time_interval = ValidateInt(settings, "hai_time_interval"); - Global.hai_time_drift = ValidateInt(settings, "hai_time_drift"); + // HAI / Leviton Omni Controller + Global.controller_address = settings["controller_address"]; + Global.controller_port = ValidatePort(settings, "controller_port"); + Global.controller_key1 = settings["controller_key1"]; + Global.controller_key2 = settings["controller_key2"]; - // mySQL Database - Global.mysql_logging = ValidateYesNo(settings, "mysql_logging"); - Global.mysql_connection = settings["mysql_connection"]; + // Controller Time Sync + Global.time_sync = ValidateYesNo(settings, "time_sync"); + Global.time_interval = ValidateInt(settings, "time_interval"); + Global.time_drift = ValidateInt(settings, "time_drift"); - // Events - Global.mail_server = settings["mail_server"]; - Global.mail_port = ValidatePort(settings, "mail_port"); - Global.mail_username = settings["mail_username"]; - Global.mail_password = settings["mail_password"]; - Global.mail_from = ValidateMailFrom(settings, "mail_from"); - Global.mail_to = ValidateMailTo(settings, "mail_to"); - Global.mail_alarm_to = ValidateMailTo(settings, "mail_alarm_to"); - - // Prowl Notifications - Global.prowl_key = ValidateMultipleStrings(settings, "prowl_key"); - Global.prowl_messages = ValidateYesNo(settings, "prowl_messages"); - - // Web Service - Global.webapi_enabled = ValidateYesNo(settings, "webapi_enabled"); - Global.webapi_port = ValidatePort(settings, "webapi_port"); - - // Verbose Output + // Verbose Console Global.verbose_unhandled = ValidateYesNo(settings, "verbose_unhandled"); Global.verbose_event = ValidateYesNo(settings, "verbose_event"); Global.verbose_area = ValidateYesNo(settings, "verbose_area"); @@ -51,6 +40,82 @@ namespace HAILogger Global.verbose_thermostat = ValidateYesNo(settings, "verbose_thermostat"); Global.verbose_unit = ValidateYesNo(settings, "verbose_unit"); Global.verbose_message = ValidateYesNo(settings, "verbose_message"); + + // mySQL Logging + Global.mysql_logging = ValidateYesNo(settings, "mysql_logging"); + Global.mysql_connection = settings["mysql_connection"]; + + // Web Service + Global.webapi_enabled = ValidateYesNo(settings, "webapi_enabled"); + Global.webapi_port = ValidatePort(settings, "webapi_port"); + + // MQTT + Global.mqtt_enabled = ValidateYesNo(settings, "mqtt_enabled"); + Global.mqtt_server = settings["mqtt_server"]; + Global.mqtt_port = ValidatePort(settings, "mqtt_port"); + Global.mqtt_username = settings["mqtt_username"]; + Global.mqtt_password = settings["mqtt_password"]; + Global.mqtt_discovery_prefix = settings["mqtt_discovery_prefix"]; + Global.mqtt_discovery_ignore_zones = ValidateRange(settings, "mqtt_discovery_ignore_zones"); + Global.mqtt_discovery_ignore_units = ValidateRange(settings, "mqtt_discovery_ignore_units"); + Global.mqtt_discovery_override_zone = LoadOverrideZone(settings, "mqtt_discovery_override_zone"); + + // Notifications + Global.notify_area = ValidateYesNo(settings, "notify_area"); + Global.notify_message = ValidateYesNo(settings, "notify_message"); + + // Email Notifications + Global.mail_server = settings["mail_server"]; + Global.mail_port = ValidatePort(settings, "mail_port"); + Global.mail_username = settings["mail_username"]; + Global.mail_password = settings["mail_password"]; + Global.mail_from = ValidateMailFrom(settings, "mail_from"); + Global.mail_to = ValidateMailTo(settings, "mail_to"); + + // Prowl Notifications + Global.prowl_key = ValidateMultipleStrings(settings, "prowl_key"); + + // Pushover Notifications + Global.pushover_token = settings["pushover_token"]; + Global.pushover_user = ValidateMultipleStrings(settings, "pushover_user"); + } + + private static ConcurrentDictionary LoadOverrideZone(NameValueCollection settings, string section) + { + try + { + ConcurrentDictionary ret = new ConcurrentDictionary(); + + if (settings[section] == null) + return ret; + + string[] ids = settings[section].Split(','); + + for (int i = 0; i < ids.Length; i++) + { + Dictionary 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") || !Int32.TryParse(attributes["id"], out int attrib_id)) + throw new Exception("Missing or invalid id attribute"); + + if (!attributes.ContainsKey("device_class") || !Enum.TryParse(attributes["device_class"], out BinarySensor.DeviceClass attrib_device_class)) + throw new Exception("Missing or invalid device_class attribute"); + + ret.TryAdd(attrib_id, new OverrideZone() + { + device_class = attrib_device_class, + }); + } + + return ret; + } + catch (Exception ex) + { + log.Error("Invalid override zone specified for " + section, ex); + throw; + } } private static int ValidateInt(NameValueCollection settings, string section) @@ -61,7 +126,20 @@ namespace HAILogger } catch { - Event.WriteError("Settings", "Invalid integer specified for " + section); + log.Error("Invalid integer specified for " + section); + throw; + } + } + + private static HashSet ValidateRange(NameValueCollection settings, string section) + { + try + { + return new HashSet(settings[section].ParseRanges()); + } + catch + { + log.Error("Invalid range specified for " + section); throw; } } @@ -79,7 +157,7 @@ namespace HAILogger } catch { - Event.WriteError("Settings", "Invalid port specified for " + section); + log.Error("Invalid port specified for " + section); throw; } } @@ -92,7 +170,7 @@ namespace HAILogger } catch { - Event.WriteError("Settings", "Invalid bool specified for " + section); + log.Error("Invalid bool specified for " + section); throw; } } @@ -111,7 +189,7 @@ namespace HAILogger } catch { - Event.WriteError("Settings", "Invalid IP specified for " + section); + log.Error("Invalid IP specified for " + section); throw; } } @@ -127,7 +205,7 @@ namespace HAILogger } catch { - Event.WriteError("Settings", "Invalid directory specified for " + section); + log.Error("Invalid directory specified for " + section); throw; } } @@ -140,7 +218,7 @@ namespace HAILogger } catch { - Event.WriteError("Settings", "Invalid email specified for " + section); + log.Error("Invalid email specified for " + section); throw; } } @@ -162,7 +240,7 @@ namespace HAILogger } catch { - Event.WriteError("Settings", "Invalid email specified for " + section); + log.Error("Invalid email specified for " + section); throw; } } @@ -178,7 +256,7 @@ namespace HAILogger } catch { - Event.WriteError("Settings", "Invalid string specified for " + section); + log.Error("Invalid string specified for " + section); throw; } } @@ -193,7 +271,7 @@ namespace HAILogger return false; else { - Event.WriteError("Settings", "Invalid yes/no specified for " + section); + log.Error("Invalid yes/no specified for " + section); throw new Exception(); } } @@ -231,9 +309,9 @@ namespace HAILogger sr.Close(); fs.Close(); } - catch (FileNotFoundException) + catch (FileNotFoundException ex) { - Event.WriteError("Settings", "Unable to parse settings file " + sFile); + log.Error("Unable to parse settings file " + sFile, ex); throw; } diff --git a/HAILogger/AreaContract.cs b/OmniLinkBridge/WebService/AreaContract.cs similarity index 94% rename from HAILogger/AreaContract.cs rename to OmniLinkBridge/WebService/AreaContract.cs index 52af3fe..c6876a0 100644 --- a/HAILogger/AreaContract.cs +++ b/OmniLinkBridge/WebService/AreaContract.cs @@ -1,6 +1,6 @@ using System.Runtime.Serialization; -namespace HAILogger +namespace OmniLinkBridge.WebAPI { [DataContract] public class AreaContract diff --git a/HAILogger/CommandContract.cs b/OmniLinkBridge/WebService/CommandContract.cs similarity index 87% rename from HAILogger/CommandContract.cs rename to OmniLinkBridge/WebService/CommandContract.cs index cb97c9f..df847d6 100644 --- a/HAILogger/CommandContract.cs +++ b/OmniLinkBridge/WebService/CommandContract.cs @@ -1,6 +1,6 @@ using System.Runtime.Serialization; -namespace HAILogger +namespace OmniLinkBridge.WebAPI { [DataContract] public class CommandContract diff --git a/HAILogger/IHAIService.cs b/OmniLinkBridge/WebService/IOmniLinkService.cs similarity index 98% rename from HAILogger/IHAIService.cs rename to OmniLinkBridge/WebService/IOmniLinkService.cs index 05d3eeb..87cdcf0 100644 --- a/HAILogger/IHAIService.cs +++ b/OmniLinkBridge/WebService/IOmniLinkService.cs @@ -2,10 +2,10 @@ using System.ServiceModel; using System.ServiceModel.Web; -namespace HAILogger +namespace OmniLinkBridge.WebAPI { [ServiceContract] - public interface IHAIService + public interface IOmniLinkService { [OperationContract] [WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)] diff --git a/HAILogger/Helper.cs b/OmniLinkBridge/WebService/MappingExtensions.cs similarity index 66% rename from HAILogger/Helper.cs rename to OmniLinkBridge/WebService/MappingExtensions.cs index 849220c..299522a 100644 --- a/HAILogger/Helper.cs +++ b/OmniLinkBridge/WebService/MappingExtensions.cs @@ -1,21 +1,22 @@ using HAI_Shared; using System; -using System.IO; -using System.Runtime.Serialization.Json; +using System.Collections.Generic; +using System.Linq; using System.Text; +using System.Threading.Tasks; -namespace HAILogger +namespace OmniLinkBridge.WebAPI { - static class Helper + public static class MappingExtensions { private static string lastmode = "OFF"; - public static AreaContract ConvertArea(ushort id, clsArea area) + public static AreaContract ToContract(this clsArea area) { AreaContract ret = new AreaContract(); - ret.id = id; - ret.name = area.Name; + ret.id = (ushort)area.Number; + ret.name = area.Name; ret.burglary = area.AreaBurglaryAlarmText; ret.co = area.AreaGasAlarmText; ret.fire = area.AreaFireAlarmText; @@ -34,11 +35,11 @@ namespace HAILogger return ret; } - public static ZoneContract ConvertZone(ushort id, clsZone zone) + public static ZoneContract ToContract(this clsZone zone) { ZoneContract ret = new ZoneContract(); - ret.id = id; + ret.id = (ushort)zone.Number; ret.zonetype = zone.ZoneType; ret.name = zone.Name; ret.status = zone.StatusText(); @@ -47,11 +48,11 @@ namespace HAILogger return ret; } - public static UnitContract ConvertUnit(ushort id, clsUnit unit) + public static UnitContract ToContract(this clsUnit unit) { UnitContract ret = new UnitContract(); - ret.id = id; + ret.id = (ushort)unit.Number; ret.name = unit.Name; if (unit.Status > 100) @@ -64,15 +65,15 @@ namespace HAILogger return ret; } - public static ThermostatContract ConvertThermostat(ushort id, clsThermostat unit) + public static ThermostatContract ToContract(this clsThermostat unit) { ThermostatContract ret = new ThermostatContract(); - ret.id = id; + 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); @@ -97,22 +98,5 @@ namespace HAILogger return ret; } - - public static int ConvertTemperature(int f) - { - // Convert to celsius - double c = 5.0 / 9.0 * (f - 32); - - // Convert to omni temp (0 is -40C and 255 is 87.5C) - return (int)Math.Round((c + 40) * 2, 0); - } - - public static string Serialize(T obj) - { - MemoryStream stream = new MemoryStream(); - DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(T)); - ser.WriteObject(stream, obj); - return Encoding.UTF8.GetString(stream.ToArray()); - } } } diff --git a/HAILogger/NameContract.cs b/OmniLinkBridge/WebService/NameContract.cs similarity index 87% rename from HAILogger/NameContract.cs rename to OmniLinkBridge/WebService/NameContract.cs index 654f0f0..0a36447 100644 --- a/HAILogger/NameContract.cs +++ b/OmniLinkBridge/WebService/NameContract.cs @@ -1,6 +1,6 @@ using System.Runtime.Serialization; -namespace HAILogger +namespace OmniLinkBridge.WebAPI { [DataContract] public class NameContract diff --git a/HAILogger/HAIService.cs b/OmniLinkBridge/WebService/OmniLinkService.cs similarity index 58% rename from HAILogger/HAIService.cs rename to OmniLinkBridge/WebService/OmniLinkService.cs index 2f8bce9..c9a528d 100644 --- a/HAILogger/HAIService.cs +++ b/OmniLinkBridge/WebService/OmniLinkService.cs @@ -1,28 +1,32 @@ using HAI_Shared; +using log4net; using System; using System.Collections.Generic; +using System.Reflection; using System.ServiceModel; using System.ServiceModel.Web; -namespace HAILogger +namespace OmniLinkBridge.WebAPI { [ServiceBehavior(IncludeExceptionDetailInFaults = true)] - public class HAIService : IHAIService + public class OmniLinkService : IOmniLinkService { + private static ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + public void Subscribe(SubscribeContract contract) { - Event.WriteVerbose("WebService", "Subscribe"); + log.Debug("Subscribe"); WebNotification.AddSubscription(contract.callback); } public List ListAreas() { - Event.WriteVerbose("WebService", "ListAreas"); + log.Debug("ListAreas"); List names = new List(); - for (ushort i = 1; i < WebService.HAC.Areas.Count; i++) + for (ushort i = 1; i < WebServiceModule.OmniLink.Controller.Areas.Count; i++) { - clsArea area = WebService.HAC.Areas[i]; + clsArea area = WebServiceModule.OmniLink.Controller.Areas[i]; if (area.DefaultProperties == false) names.Add(new NameContract() { id = i, name = area.Name }); @@ -32,23 +36,22 @@ namespace HAILogger public AreaContract GetArea(ushort id) { - Event.WriteVerbose("WebService", "GetArea: " + id); + log.Debug("GetArea: " + id); WebOperationContext ctx = WebOperationContext.Current; ctx.OutgoingResponse.Headers.Add("type", "area"); - clsArea area = WebService.HAC.Areas[id]; - return Helper.ConvertArea(id, area); + return WebServiceModule.OmniLink.Controller.Areas[id].ToContract(); } public List ListZonesContact() { - Event.WriteVerbose("WebService", "ListZonesContact"); + log.Debug("ListZonesContact"); List names = new List(); - for (ushort i = 1; i < WebService.HAC.Zones.Count; i++) + for (ushort i = 1; i < WebServiceModule.OmniLink.Controller.Zones.Count; i++) { - clsZone zone = WebService.HAC.Zones[i]; + clsZone zone = WebServiceModule.OmniLink.Controller.Zones[i]; if ((zone.ZoneType == enuZoneType.EntryExit || zone.ZoneType == enuZoneType.X2EntryDelay || @@ -63,12 +66,12 @@ namespace HAILogger public List ListZonesMotion() { - Event.WriteVerbose("WebService", "ListZonesMotion"); + log.Debug("ListZonesMotion"); List names = new List(); - for (ushort i = 1; i < WebService.HAC.Zones.Count; i++) + for (ushort i = 1; i < WebServiceModule.OmniLink.Controller.Zones.Count; i++) { - clsZone zone = WebService.HAC.Zones[i]; + clsZone zone = WebServiceModule.OmniLink.Controller.Zones[i]; if ((zone.ZoneType == enuZoneType.AwayInt || zone.ZoneType == enuZoneType.NightInt) && zone.DefaultProperties == false) @@ -79,12 +82,12 @@ namespace HAILogger public List ListZonesWater() { - Event.WriteVerbose("WebService", "ListZonesWater"); + log.Debug("ListZonesWater"); List names = new List(); - for (ushort i = 1; i < WebService.HAC.Zones.Count; i++) + for (ushort i = 1; i < WebServiceModule.OmniLink.Controller.Zones.Count; i++) { - clsZone zone = WebService.HAC.Zones[i]; + clsZone zone = WebServiceModule.OmniLink.Controller.Zones[i]; if (zone.ZoneType == enuZoneType.Water && zone.DefaultProperties == false) names.Add(new NameContract() { id = i, name = zone.Name }); @@ -94,12 +97,12 @@ namespace HAILogger public List ListZonesSmoke() { - Event.WriteVerbose("WebService", "ListZonesSmoke"); + log.Debug("ListZonesSmoke"); List names = new List(); - for (ushort i = 1; i < WebService.HAC.Zones.Count; i++) + for (ushort i = 1; i < WebServiceModule.OmniLink.Controller.Zones.Count; i++) { - clsZone zone = WebService.HAC.Zones[i]; + clsZone zone = WebServiceModule.OmniLink.Controller.Zones[i]; if (zone.ZoneType == enuZoneType.Fire && zone.DefaultProperties == false) names.Add(new NameContract() { id = i, name = zone.Name }); @@ -109,12 +112,12 @@ namespace HAILogger public List ListZonesCO() { - Event.WriteVerbose("WebService", "ListZonesCO"); + log.Debug("ListZonesCO"); List names = new List(); - for (ushort i = 1; i < WebService.HAC.Zones.Count; i++) + for (ushort i = 1; i < WebServiceModule.OmniLink.Controller.Zones.Count; i++) { - clsZone zone = WebService.HAC.Zones[i]; + clsZone zone = WebServiceModule.OmniLink.Controller.Zones[i]; if (zone.ZoneType == enuZoneType.Gas && zone.DefaultProperties == false) names.Add(new NameContract() { id = i, name = zone.Name }); @@ -124,12 +127,12 @@ namespace HAILogger public List ListZonesTemp() { - Event.WriteVerbose("WebService", "ListZonesTemp"); + log.Debug("ListZonesTemp"); List names = new List(); - for (ushort i = 1; i < WebService.HAC.Zones.Count; i++) + for (ushort i = 1; i < WebServiceModule.OmniLink.Controller.Zones.Count; i++) { - clsZone zone = WebService.HAC.Zones[i]; + clsZone zone = WebServiceModule.OmniLink.Controller.Zones[i]; if (zone.IsTemperatureZone() && zone.DefaultProperties == false) names.Add(new NameContract() { id = i, name = zone.Name }); @@ -139,17 +142,17 @@ namespace HAILogger public ZoneContract GetZone(ushort id) { - Event.WriteVerbose("WebService", "GetZone: " + id); + log.Debug("GetZone: " + id); WebOperationContext ctx = WebOperationContext.Current; - if (WebService.HAC.Zones[id].IsTemperatureZone()) + if (WebServiceModule.OmniLink.Controller.Zones[id].IsTemperatureZone()) { ctx.OutgoingResponse.Headers.Add("type", "temp"); } else { - switch (WebService.HAC.Zones[id].ZoneType) + switch (WebServiceModule.OmniLink.Controller.Zones[id].ZoneType) { case enuZoneType.EntryExit: case enuZoneType.X2EntryDelay: @@ -178,18 +181,17 @@ namespace HAILogger } } - clsZone unit = WebService.HAC.Zones[id]; - return Helper.ConvertZone(id, unit); + return WebServiceModule.OmniLink.Controller.Zones[id].ToContract(); } public List ListUnits() { - Event.WriteVerbose("WebService", "ListUnits"); + log.Debug("ListUnits"); List names = new List(); - for (ushort i = 1; i < WebService.HAC.Units.Count; i++) + for (ushort i = 1; i < WebServiceModule.OmniLink.Controller.Units.Count; i++) { - clsUnit unit = WebService.HAC.Units[i]; + clsUnit unit = WebServiceModule.OmniLink.Controller.Units[i]; if (unit.DefaultProperties == false) names.Add(new NameContract() { id = i, name = unit.Name }); @@ -199,42 +201,41 @@ namespace HAILogger public UnitContract GetUnit(ushort id) { - Event.WriteVerbose("WebService", "GetUnit: " + id); + log.Debug("GetUnit: " + id); WebOperationContext ctx = WebOperationContext.Current; ctx.OutgoingResponse.Headers.Add("type", "unit"); - clsUnit unit = WebService.HAC.Units[id]; - return Helper.ConvertUnit(id, unit); + return WebServiceModule.OmniLink.Controller.Units[id].ToContract(); } public void SetUnit(CommandContract unit) { - Event.WriteVerbose("WebService", "SetUnit: " + unit.id + " to " + unit.value + "%"); + log.Debug("SetUnit: " + unit.id + " to " + unit.value + "%"); if (unit.value == 0) - WebService.HAC.SendCommand(enuUnitCommand.Off, 0, unit.id); + WebServiceModule.OmniLink.Controller.SendCommand(enuUnitCommand.Off, 0, unit.id); else if (unit.value == 100) - WebService.HAC.SendCommand(enuUnitCommand.On, 0, unit.id); + WebServiceModule.OmniLink.Controller.SendCommand(enuUnitCommand.On, 0, unit.id); else - WebService.HAC.SendCommand(enuUnitCommand.Level, BitConverter.GetBytes(unit.value)[0], unit.id); + WebServiceModule.OmniLink.Controller.SendCommand(enuUnitCommand.Level, BitConverter.GetBytes(unit.value)[0], unit.id); } public void SetUnitKeypadPress(CommandContract unit) { - Event.WriteVerbose("WebService", "SetUnitKeypadPress: " + unit.id + " to " + unit.value + " button"); - WebService.HAC.SendCommand(enuUnitCommand.LutronHomeWorksKeypadButtonPress, BitConverter.GetBytes(unit.value)[0], unit.id); + log.Debug("SetUnitKeypadPress: " + unit.id + " to " + unit.value + " button"); + WebServiceModule.OmniLink.Controller.SendCommand(enuUnitCommand.LutronHomeWorksKeypadButtonPress, BitConverter.GetBytes(unit.value)[0], unit.id); } public List ListThermostats() { - Event.WriteVerbose("WebService", "ListThermostats"); + log.Debug("ListThermostats"); List names = new List(); - for (ushort i = 1; i < WebService.HAC.Thermostats.Count; i++) + for (ushort i = 1; i < WebServiceModule.OmniLink.Controller.Thermostats.Count; i++) { - clsThermostat unit = WebService.HAC.Thermostats[i]; + clsThermostat unit = WebServiceModule.OmniLink.Controller.Thermostats[i]; if (unit.DefaultProperties == false) names.Add(new NameContract() { id = i, name = unit.Name }); @@ -244,55 +245,54 @@ namespace HAILogger public ThermostatContract GetThermostat(ushort id) { - Event.WriteVerbose("WebService", "GetThermostat: " + id); + log.Debug("GetThermostat: " + id); WebOperationContext ctx = WebOperationContext.Current; ctx.OutgoingResponse.Headers.Add("type", "thermostat"); - clsThermostat unit = WebService.HAC.Thermostats[id]; - return Helper.ConvertThermostat(id, unit); + return WebServiceModule.OmniLink.Controller.Thermostats[id].ToContract(); } public void SetThermostatCoolSetpoint(CommandContract unit) { - int temp = Helper.ConvertTemperature(unit.value); - Event.WriteVerbose("WebService", "SetThermostatCoolSetpoint: " + unit.id + " to " + unit.value + "F (" + temp + ")"); - WebService.HAC.SendCommand(enuUnitCommand.SetHighSetPt, BitConverter.GetBytes(temp)[0], unit.id); + int temp = ((double)unit.value).ToCelsius().ToOmniTemp(); + log.Debug("SetThermostatCoolSetpoint: " + unit.id + " to " + unit.value + "F (" + temp + ")"); + WebServiceModule.OmniLink.Controller.SendCommand(enuUnitCommand.SetHighSetPt, BitConverter.GetBytes(temp)[0], unit.id); } public void SetThermostatHeatSetpoint(CommandContract unit) { - int temp = Helper.ConvertTemperature(unit.value); - Event.WriteVerbose("WebService", "SetThermostatCoolSetpoint: " + unit.id + " to " + unit.value + "F (" + temp + ")"); - WebService.HAC.SendCommand(enuUnitCommand.SetLowSetPt, BitConverter.GetBytes(temp)[0], unit.id); + int temp = ((double)unit.value).ToCelsius().ToOmniTemp(); + log.Debug("SetThermostatCoolSetpoint: " + unit.id + " to " + unit.value + "F (" + temp + ")"); + WebServiceModule.OmniLink.Controller.SendCommand(enuUnitCommand.SetLowSetPt, BitConverter.GetBytes(temp)[0], unit.id); } public void SetThermostatMode(CommandContract unit) { - Event.WriteVerbose("WebService", "SetThermostatMode: " + unit.id + " to " + unit.value); - WebService.HAC.SendCommand(enuUnitCommand.Mode, BitConverter.GetBytes(unit.value)[0], unit.id); + log.Debug("SetThermostatMode: " + unit.id + " to " + unit.value); + WebServiceModule.OmniLink.Controller.SendCommand(enuUnitCommand.Mode, BitConverter.GetBytes(unit.value)[0], unit.id); } public void SetThermostatFanMode(CommandContract unit) { - Event.WriteVerbose("WebService", "SetThermostatFanMode: " + unit.id + " to " + unit.value); - WebService.HAC.SendCommand(enuUnitCommand.Fan, BitConverter.GetBytes(unit.value)[0], unit.id); + log.Debug("SetThermostatFanMode: " + unit.id + " to " + unit.value); + WebServiceModule.OmniLink.Controller.SendCommand(enuUnitCommand.Fan, BitConverter.GetBytes(unit.value)[0], unit.id); } public void SetThermostatHold(CommandContract unit) { - Event.WriteVerbose("WebService", "SetThermostatHold: " + unit.id + " to " + unit.value); - WebService.HAC.SendCommand(enuUnitCommand.Hold, BitConverter.GetBytes(unit.value)[0], unit.id); + log.Debug("SetThermostatHold: " + unit.id + " to " + unit.value); + WebServiceModule.OmniLink.Controller.SendCommand(enuUnitCommand.Hold, BitConverter.GetBytes(unit.value)[0], unit.id); } public List ListButtons() { - Event.WriteVerbose("WebService", "ListButtons"); + log.Debug("ListButtons"); List names = new List(); - for (ushort i = 1; i < WebService.HAC.Buttons.Count; i++) + for (ushort i = 1; i < WebServiceModule.OmniLink.Controller.Buttons.Count; i++) { - clsButton unit = WebService.HAC.Buttons[i]; + clsButton unit = WebServiceModule.OmniLink.Controller.Buttons[i]; if (unit.DefaultProperties == false) names.Add(new NameContract() { id = i, name = unit.Name }); @@ -302,8 +302,8 @@ namespace HAILogger public void PushButton(CommandContract unit) { - Event.WriteVerbose("WebService", "PushButton: " + unit.id); - WebService.HAC.SendCommand(enuUnitCommand.Button, 0, unit.id); + log.Debug("PushButton: " + unit.id); + WebServiceModule.OmniLink.Controller.SendCommand(enuUnitCommand.Button, 0, unit.id); } } } \ No newline at end of file diff --git a/HAILogger/SubscribeContract.cs b/OmniLinkBridge/WebService/SubscribeContract.cs similarity index 84% rename from HAILogger/SubscribeContract.cs rename to OmniLinkBridge/WebService/SubscribeContract.cs index 2448246..24d78d1 100644 --- a/HAILogger/SubscribeContract.cs +++ b/OmniLinkBridge/WebService/SubscribeContract.cs @@ -1,6 +1,6 @@ using System.Runtime.Serialization; -namespace HAILogger +namespace OmniLinkBridge.WebAPI { [DataContract] public class SubscribeContract diff --git a/HAILogger/ThermostatContract.cs b/OmniLinkBridge/WebService/ThermostatContract.cs similarity index 96% rename from HAILogger/ThermostatContract.cs rename to OmniLinkBridge/WebService/ThermostatContract.cs index 1691225..727d98a 100644 --- a/HAILogger/ThermostatContract.cs +++ b/OmniLinkBridge/WebService/ThermostatContract.cs @@ -1,7 +1,7 @@ using HAI_Shared; using System.Runtime.Serialization; -namespace HAILogger +namespace OmniLinkBridge.WebAPI { [DataContract] public class ThermostatContract diff --git a/HAILogger/UnitContract.cs b/OmniLinkBridge/WebService/UnitContract.cs similarity index 90% rename from HAILogger/UnitContract.cs rename to OmniLinkBridge/WebService/UnitContract.cs index 69563a6..9c8a04c 100644 --- a/HAILogger/UnitContract.cs +++ b/OmniLinkBridge/WebService/UnitContract.cs @@ -1,6 +1,6 @@ using System.Runtime.Serialization; -namespace HAILogger +namespace OmniLinkBridge.WebAPI { [DataContract] public class UnitContract diff --git a/OmniLinkBridge/WebService/WebNotification.cs b/OmniLinkBridge/WebService/WebNotification.cs new file mode 100644 index 0000000..98e9483 --- /dev/null +++ b/OmniLinkBridge/WebService/WebNotification.cs @@ -0,0 +1,107 @@ +using log4net; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Reflection; + +namespace OmniLinkBridge.WebAPI +{ + static class WebNotification + { + private static ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + private static List subscriptions = new List(); + private static object subscriptions_lock = new object(); + + public static void AddSubscription(string callback) + { + lock (subscriptions_lock) + { + if (!subscriptions.Contains(callback)) + { + log.Debug("Adding subscription to " + callback); + subscriptions.Add(callback); + } + } + } + + public static void Send(string type, string body) + { + string[] send; + lock (subscriptions_lock) + send = subscriptions.ToArray(); + + foreach (string subscription in send) + { + WebClient client = new WebClient(); + client.Headers.Add(HttpRequestHeader.ContentType, "application/json"); + client.Headers.Add("type", type); + client.UploadStringCompleted += client_UploadStringCompleted; + + try + { + client.UploadStringAsync(new Uri(subscription), "POST", body, subscription); + } + catch (Exception ex) + { + log.Error("An error occurred sending notification to " + subscription, ex); + subscriptions.Remove(subscription); + } + } + } + + static void client_UploadStringCompleted(object sender, UploadStringCompletedEventArgs e) + { + if (e.Error != null) + { + log.Error("An error occurred sending notification to " + e.UserState.ToString(), e.Error); + + lock (subscriptions_lock) + subscriptions.Remove(e.UserState.ToString()); + } + } + + public static void RestoreSubscriptions() + { + string json; + + try + { + if (File.Exists(Global.webapi_subscriptions_file)) + json = File.ReadAllText(Global.webapi_subscriptions_file); + else + return; + + lock (subscriptions_lock) + subscriptions = JsonConvert.DeserializeObject>(json); + + log.Debug("Restored subscriptions from file"); + } + catch (Exception ex) + { + log.Error("An error occurred restoring subscriptions", ex); + } + } + + public static void SaveSubscriptions() + { + string json; + + lock (subscriptions_lock) + json = JsonConvert.SerializeObject(subscriptions); + + try + { + File.WriteAllText(Global.webapi_subscriptions_file, json); + + log.Debug("Saved subscriptions to file"); + } + catch (Exception ex) + { + log.Error("An error occurred saving subscriptions", ex); + } + } + } +} \ No newline at end of file diff --git a/HAILogger/ZoneContract.cs b/OmniLinkBridge/WebService/ZoneContract.cs similarity index 93% rename from HAILogger/ZoneContract.cs rename to OmniLinkBridge/WebService/ZoneContract.cs index 55609e9..1a48322 100644 --- a/HAILogger/ZoneContract.cs +++ b/OmniLinkBridge/WebService/ZoneContract.cs @@ -1,7 +1,7 @@ using HAI_Shared; using System.Runtime.Serialization; -namespace HAILogger +namespace OmniLinkBridge.WebAPI { [DataContract] public class ZoneContract diff --git a/OmniLinkBridgeTest/AssemblyTestHarness.cs b/OmniLinkBridgeTest/AssemblyTestHarness.cs new file mode 100644 index 0000000..23b5dff --- /dev/null +++ b/OmniLinkBridgeTest/AssemblyTestHarness.cs @@ -0,0 +1,19 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OmniLinkBridge; +using System.IO; +using System.Reflection; + +namespace OmniLinkBridgeTest +{ + [TestClass] + public class AssemblyTestHarness + { + [AssemblyInitialize] + public static void InitializeAssembly(TestContext context) + { + Global.config_file = "OmniLinkBridge.ini"; + Settings.LoadSettings(); + } + } +} diff --git a/OmniLinkBridgeTest/ExtensionTest.cs b/OmniLinkBridgeTest/ExtensionTest.cs new file mode 100644 index 0000000..1bc6345 --- /dev/null +++ b/OmniLinkBridgeTest/ExtensionTest.cs @@ -0,0 +1,22 @@ +using System; +using System.Text; +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OmniLinkBridge; + +namespace OmniLinkBridgeTest +{ + [TestClass] + public class ExtensionTest + { + [TestMethod] + public void TestParseRange() + { + List blank = "".ParseRanges(); + Assert.AreEqual(0, blank.Count); + + List range = "1-3,5,6".ParseRanges(); + CollectionAssert.AreEqual(new List(new int[] { 1, 2, 3, 5, 6 }), range); + } + } +} diff --git a/OmniLinkBridgeTest/NotificationTest.cs b/OmniLinkBridgeTest/NotificationTest.cs new file mode 100644 index 0000000..ee21f49 --- /dev/null +++ b/OmniLinkBridgeTest/NotificationTest.cs @@ -0,0 +1,18 @@ +using System; +using System.Text; +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OmniLinkBridge.Notifications; + +namespace OmniLinkBridgeTest +{ + [TestClass] + public class NotificationTest + { + [TestMethod] + public void SendNotification() + { + Notification.Notify("Title", "Description"); + } + } +} diff --git a/OmniLinkBridgeTest/OmniLinkBridgeTest.csproj b/OmniLinkBridgeTest/OmniLinkBridgeTest.csproj new file mode 100644 index 0000000..80e8ca9 --- /dev/null +++ b/OmniLinkBridgeTest/OmniLinkBridgeTest.csproj @@ -0,0 +1,67 @@ + + + + + Debug + AnyCPU + {6E6950E4-35F9-4D99-8ADA-B7E2F29D4172} + Library + Properties + OmniLinkBridgeTest + OmniLinkBridgeTest + v4.5.2 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 15.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + 1.3.2 + + + 1.3.2 + + + + + {0a636707-98ba-45ab-9843-aed430933cee} + OmniLinkBridge + + + + + \ No newline at end of file diff --git a/OmniLinkBridgeTest/Properties/AssemblyInfo.cs b/OmniLinkBridgeTest/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..7ed7010 --- /dev/null +++ b/OmniLinkBridgeTest/Properties/AssemblyInfo.cs @@ -0,0 +1,20 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("OmniLinkBridgeTest")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Excalibur Partners, LLC")] +[assembly: AssemblyProduct("OmniLinkBridgeTest")] +[assembly: AssemblyCopyright("Copyright © Excalibur Partners, LLC 2018")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: ComVisible(false)] + +[assembly: Guid("6e6950e4-35f9-4d99-8ada-b7e2f29d4172")] + +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/README.md b/README.md index 95bbb7d..c8370dd 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,13 @@ -# HAILogger -Provides logging and web service API for HAI/Leviton OmniPro II controllers +# OmniLink Bridge +Provides time sync, logging, web service API, and MQTT bridge for HAI/Leviton OmniPro II controllers -##Download -You can download the [binary here](http://www.excalibur-partners.com/downloads/HAILogger_1_0_8.zip) +## Download +You can download the [binary here](http://www.excalibur-partners.com/downloads/OmniLinkBridge_1_1_0.zip) -##Requirements -- .NET Framework 4.0 -- mySQL 5.1 ODBC Connector +## Requirements +- .NET Framework 4.5.2 -##Operation +## 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 @@ -16,63 +15,124 @@ You can download the [binary here](http://www.excalibur-partners.com/downloads/H - 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 -##Notifications -- Emails are sent to mail_alarm_to when an area status changes -- Prowl notifications are sent when an areas status changes +## Notifications +- Supports email, prowl, and pushover +- Always sent for area alarms and critical system events +- Optionally enable for area status changes and console messages -##Installation Windows -1. Copy files to your desired location like C:\HAILogger -2. Edit HAILogger.ini and define at a minimum the controller IP and encryptions keys -3. Run HAILogger.exe to verify connectivity -4. For Windows Service run install.bat / uninstall.bat -5. Start service from Administrative Tools -> Services +## 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 +4. Add Windows service + - sc create OmniLinkBridge binpath=C:\OmniLinkBridge\OmniLinkBridge.exe +5. Start service + - net start OmniLinkBridge -##Installation Linux -1. Copy files to your desired location like /opt/HAILogger +## Installation Linux +1. Copy files to your desired location like /opt/OmniLinkBridge 2. Configure at a minimum the controller IP and encryptions keys - - vim HAILogger.ini + - vim OmniLinkBridge.ini 3. Run as interactive to verify connectivity - - ./HAILogger.exe -i -4. Add systemd file and configure ExecStart path - - cp hailogger.service /etc/systemd/system/ - - vim /etc/systemd/system/hailogger.service + - mono OmniLinkBridge.exe -i +4. Add systemd file and configure paths + - cp omnilinkbridge.service /etc/systemd/system/ + - vim /etc/systemd/system/omnilinkbridge.service + - systemctl daemon-reload 5. Enable at boot and start service - - systemctl enable hailogger.service - - systemctl start hailogger.service + - systemctl enable omnilinkbridge.service + - systemctl start omnilinkbridge.service -##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 HAI Logger uses ODBC to communicate with the database. The MySQL ODBC Connector library is needed for Windows ODBC to communicate with MySQL. Make sure you install version 5.1 of the MySQL ODBC Connector provided in the link below. +## 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. http://dev.mysql.com/downloads/mysql/ http://dev.mysql.com/downloads/tools/workbench/ -http://dev.mysql.com/downloads/connector/odbc/5.1.html +http://dev.mysql.com/downloads/connector/odbc/ -After installing MySQL server it should have asked you to setup an instance. One of the steps of the instance wizard was to create a root password. Assuming you installed the HAI Logger on the same computer you will want to use the below settings in HAILogger.ini. - -mysql_server = localhost - -mysql_user = root - -mysql_password = password you set in the wizard - -At this point we need to open MySQL Workbench to create the database (called a schema in the Workbench GUI) for HAILogger to use. +At this point we need to open MySQL Workbench to create the database (called a schema in the Workbench GUI) for OmniLinkBridge to use. 1. After opening the program double-click on "Local instance MySQL" and enter the password you set in the wizard. 2. On the toolbar click the "Create a new schema" button, provide a name, and click apply. 3. On the left side right-click on the schema you created and click "Set as default schema". -4. In the middle section under Query1 click the open file icon and select the HAILogger.sql file. +4. In the middle section under Query1 click the open file icon and select the OmniLinkBridge.sql file. 5. Click the Execute lighting bolt to run the query, which will create the tables. -Lastly in HAILogger.ini set mysql_database to the name of the schema you created. This should get you up and running. The MySQL Workbench can also be used to view the data that HAILogger inserts into the tables. +Lastly in OmniLinkBridge.ini set mysql_connection. This should get you up and running. The MySQL Workbench can also be used to view the data that OmniLink Bridge inserts into the tables. -##Web Service API +mysql_connection = DRIVER={MySQL ODBC 8.0 Driver};SERVER=localhost;DATABASE=OmniLinkBridge;USER=root;PASSWORD=myPassword;OPTION=3; + +## 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 - Invoke-WebRequest -Uri "http://localhost:8000/SetUnit" -Method POST -ContentType "application/json" -Body (convertto-json -InputObject @{"id"=1;"value"=100}) -UseBasicParsing -##Change Log +## MQTT +This module will also publish discovery topics for Home Assistant to auto configure devices. + +SUB omnilink/areaX/state +string triggered, pending, armed_night, armed_home, armed_away, disarmed + +PUB omnilink/areaX/command +string ARM_HOME, ARM_AWAY, ARM_NIGHT, DISARM, ARM_HOME_INSTANT, ARM_NIGHT_DELAY, ARM_VACATION + +SUB omnilink/unitX/state +PUB omnilink/unitX/command +string OFF, ON + +SUB omnilink/unitX/brightness_state +PUB omnilink/unitX/brightness_command +int Level from 0 to 100 percent + +SUB omnilink/thermostatX/current_operation +string idle, cool, heat + +SUB omnilink/thermostatX/current_temperature +int Current temperature in degrees fahrenheit + +SUB omnilink/thermostatX/current_humidity +int Current relative humidity + +SUB omnilink/thermostatX/temperature_heat_state +SUB omnilink/thermostatX/temperature_cool_state +PUB omnilink/thermostatX/temperature_heat_command +PUB omnilink/thermostatX/temperature_cool_command +int Setpoint in degrees fahrenheit + +SUB omnilink/thermostatX/humidify_state +SUB omnilink/thermostatX/dehumidify_state +PUB omnilink/thermostatX/humidify_command +PUB omnilink/thermostatX/dehumidify_command +int Setpoint in relative humidity + +SUB omnilink/thermostatX/mode_state +PUB omnilink/thermostatX/mode_command +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 +PUB omnilink/thermostatX/hold_command +string off, hold + +SUB omnilink/buttonX/state +string OFF + +PUB omnilink/buttonX/command +string ON + +## Change Log +Version 1.1.0 - 2018-10-13 +- Renamed to OmniLinkBridge +- Restructured code to be event based with modules +- Added MQTT module for Home Assistant +- Added pushover notifications +- Added web service API subscriptions file to persist subscriptions + Version 1.0.8 - 2016-11-28 - Fixed web service threading when multiple subscriptions exist - Added additional zone types to contact and motion web service API