diff --git a/HAILogger.sln b/HAILogger.sln new file mode 100644 index 0000000..62ae7fd --- /dev/null +++ b/HAILogger.sln @@ -0,0 +1,22 @@ + +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 new file mode 100644 index 0000000..966d52a --- /dev/null +++ b/HAILogger/App.config @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/HAILogger/AreaContract.cs b/HAILogger/AreaContract.cs new file mode 100644 index 0000000..52af3fe --- /dev/null +++ b/HAILogger/AreaContract.cs @@ -0,0 +1,29 @@ +using System.Runtime.Serialization; + +namespace HAILogger +{ + [DataContract] + public class AreaContract + { + [DataMember] + public ushort id { get; set; } + + [DataMember] + public string name { get; set; } + + [DataMember] + public string burglary { get; set; } + + [DataMember] + public string co { get; set; } + + [DataMember] + public string fire { get; set; } + + [DataMember] + public string water { get; set; } + + [DataMember] + public string mode { get; set; } + } +} diff --git a/HAILogger/CommandContract.cs b/HAILogger/CommandContract.cs new file mode 100644 index 0000000..cb97c9f --- /dev/null +++ b/HAILogger/CommandContract.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; + +namespace HAILogger +{ + [DataContract] + public class CommandContract + { + [DataMember] + public ushort id { get; set; } + + [DataMember] + public ushort value { get; set; } + } +} diff --git a/HAILogger/CoreServer.cs b/HAILogger/CoreServer.cs new file mode 100644 index 0000000..85a54ce --- /dev/null +++ b/HAILogger/CoreServer.cs @@ -0,0 +1,1214 @@ +using HAI_Shared; +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Odbc; +using System.IO; +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() + { + Global.event_log = "EventLog.txt"; + Global.event_source = "HAI Logger"; + + if (string.IsNullOrEmpty(Global.dir_config)) + Global.dir_config = Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location); + + Settings.LoadSettings(); + + Event.WriteInfo("CoreServer", "Starting up server"); + + 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); + 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.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: + WebNotification.Send("contact", Helper.Serialize(Helper.ConvertZone( + MSG.ObjectNumber(i), HAC.Zones[MSG.ObjectNumber(i)]))); + break; + case enuZoneType.AwayInt: + 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 + + #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) + 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 + } +} diff --git a/HAILogger/Event.cs b/HAILogger/Event.cs new file mode 100644 index 0000000..ce09c39 --- /dev/null +++ b/HAILogger/Event.cs @@ -0,0 +1,165 @@ +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.dir_config + "\\" + Global.event_log, 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 new file mode 100644 index 0000000..266d693 --- /dev/null +++ b/HAILogger/Global.cs @@ -0,0 +1,54 @@ +using System.Net.Mail; + +namespace HAILogger +{ + public abstract class Global + { + // Events Preset + public static string event_log; + public static string event_source; + + // Directories + public static string dir_config; + + // 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/HAI.Controller.dll b/HAILogger/HAI.Controller.dll new file mode 100644 index 0000000..0c6700c Binary files /dev/null and b/HAILogger/HAI.Controller.dll differ diff --git a/HAILogger/HAILogger.csproj b/HAILogger/HAILogger.csproj new file mode 100644 index 0000000..627e6c3 --- /dev/null +++ b/HAILogger/HAILogger.csproj @@ -0,0 +1,107 @@ + + + + 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 new file mode 100644 index 0000000..7677909 --- /dev/null +++ b/HAILogger/HAILogger.ini @@ -0,0 +1,55 @@ +# 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/HAIService.cs b/HAILogger/HAIService.cs new file mode 100644 index 0000000..130f2b0 --- /dev/null +++ b/HAILogger/HAIService.cs @@ -0,0 +1,275 @@ +using HAI_Shared; +using System; +using System.Collections.Generic; +using System.ServiceModel; +using System.ServiceModel.Web; + +namespace HAILogger +{ + [ServiceBehavior(IncludeExceptionDetailInFaults = true)] + public class HAIService : IHAIService + { + public void Subscribe(SubscribeContract contract) + { + Event.WriteVerbose("WebService", "Subscribe"); + WebNotification.AddSubscription(contract.callback); + } + + public List ListAreas() + { + Event.WriteVerbose("WebService", "ListAreas"); + + List names = new List(); + for (ushort i = 1; i < WebService.HAC.Areas.Count; i++) + { + clsArea area = WebService.HAC.Areas[i]; + + if (area.DefaultProperties == false) + names.Add(new NameContract() { id = i, name = area.Name }); + } + return names; + } + + public AreaContract GetArea(ushort id) + { + Event.WriteVerbose("WebService", "GetArea: " + id); + + WebOperationContext ctx = WebOperationContext.Current; + ctx.OutgoingResponse.Headers.Add("type", "area"); + + clsArea area = WebService.HAC.Areas[id]; + return Helper.ConvertArea(id, area); + } + + public List ListZonesContact() + { + Event.WriteVerbose("WebService", "ListZonesContact"); + + List names = new List(); + for (ushort i = 1; i < WebService.HAC.Zones.Count; i++) + { + clsZone zone = WebService.HAC.Zones[i]; + + if ((zone.ZoneType == enuZoneType.EntryExit || + zone.ZoneType == enuZoneType.X2EntryDelay || + zone.ZoneType == enuZoneType.X4EntryDelay || + zone.ZoneType == enuZoneType.Perimeter) && zone.DefaultProperties == false) + names.Add(new NameContract() { id = i, name = zone.Name }); + } + return names; + } + + public List ListZonesMotion() + { + Event.WriteVerbose("WebService", "ListZonesMotion"); + + List names = new List(); + for (ushort i = 1; i < WebService.HAC.Zones.Count; i++) + { + clsZone zone = WebService.HAC.Zones[i]; + + if (zone.ZoneType == enuZoneType.AwayInt && zone.DefaultProperties == false) + names.Add(new NameContract() { id = i, name = zone.Name }); + } + return names; + } + + public List ListZonesWater() + { + Event.WriteVerbose("WebService", "ListZonesWater"); + + List names = new List(); + for (ushort i = 1; i < WebService.HAC.Zones.Count; i++) + { + clsZone zone = WebService.HAC.Zones[i]; + + if (zone.ZoneType == enuZoneType.Water && zone.DefaultProperties == false) + names.Add(new NameContract() { id = i, name = zone.Name }); + } + return names; + } + + public List ListZonesSmoke() + { + Event.WriteVerbose("WebService", "ListZonesSmoke"); + + List names = new List(); + for (ushort i = 1; i < WebService.HAC.Zones.Count; i++) + { + clsZone zone = WebService.HAC.Zones[i]; + + if (zone.ZoneType == enuZoneType.Fire && zone.DefaultProperties == false) + names.Add(new NameContract() { id = i, name = zone.Name }); + } + return names; + } + + public List ListZonesCO() + { + Event.WriteVerbose("WebService", "ListZonesCO"); + + List names = new List(); + for (ushort i = 1; i < WebService.HAC.Zones.Count; i++) + { + clsZone zone = WebService.HAC.Zones[i]; + + if (zone.ZoneType == enuZoneType.Gas && zone.DefaultProperties == false) + names.Add(new NameContract() { id = i, name = zone.Name }); + } + return names; + } + + public ZoneContract GetZone(ushort id) + { + Event.WriteVerbose("WebService", "GetZone: " + id); + + WebOperationContext ctx = WebOperationContext.Current; + + switch (WebService.HAC.Zones[id].ZoneType) + { + case enuZoneType.EntryExit: + case enuZoneType.X2EntryDelay: + case enuZoneType.X4EntryDelay: + case enuZoneType.Perimeter: + ctx.OutgoingResponse.Headers.Add("type", "contact"); + break; + case enuZoneType.AwayInt: + ctx.OutgoingResponse.Headers.Add("type", "motion"); + break; + case enuZoneType.Water: + ctx.OutgoingResponse.Headers.Add("type", "water"); + break; + case enuZoneType.Fire: + ctx.OutgoingResponse.Headers.Add("type", "smoke"); + break; + case enuZoneType.Gas: + ctx.OutgoingResponse.Headers.Add("type", "co"); + break; + default: + ctx.OutgoingResponse.Headers.Add("type", "unknown"); + break; + } + + clsZone unit = WebService.HAC.Zones[id]; + return Helper.ConvertZone(id, unit); + } + + public List ListUnits() + { + Event.WriteVerbose("WebService", "ListUnits"); + + List names = new List(); + for (ushort i = 1; i < WebService.HAC.Units.Count; i++) + { + clsUnit unit = WebService.HAC.Units[i]; + + if (unit.DefaultProperties == false) + names.Add(new NameContract() { id = i, name = unit.Name }); + } + return names; + } + + public UnitContract GetUnit(ushort id) + { + Event.WriteVerbose("WebService", "GetUnit: " + id); + + WebOperationContext ctx = WebOperationContext.Current; + ctx.OutgoingResponse.Headers.Add("type", "unit"); + + clsUnit unit = WebService.HAC.Units[id]; + return Helper.ConvertUnit(id, unit); + } + + public void SetUnit(CommandContract unit) + { + Event.WriteVerbose("WebService", "SetUnit: " + unit.id + " to " + unit.value + "%"); + + if (unit.value == 0) + WebService.HAC.SendCommand(enuUnitCommand.Off, 0, unit.id); + else if (unit.value == 100) + WebService.HAC.SendCommand(enuUnitCommand.On, 0, unit.id); + else + WebService.HAC.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); + } + + public List ListThermostats() + { + Event.WriteVerbose("WebService", "ListThermostats"); + + List names = new List(); + for (ushort i = 1; i < WebService.HAC.Thermostats.Count; i++) + { + clsThermostat unit = WebService.HAC.Thermostats[i]; + + if (unit.DefaultProperties == false) + names.Add(new NameContract() { id = i, name = unit.Name }); + } + return names; + } + + public ThermostatContract GetThermostat(ushort id) + { + Event.WriteVerbose("WebService", "GetThermostat: " + id); + + WebOperationContext ctx = WebOperationContext.Current; + ctx.OutgoingResponse.Headers.Add("type", "thermostat"); + + clsThermostat unit = WebService.HAC.Thermostats[id]; + return Helper.ConvertThermostat(id, unit); + } + + 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); + } + + 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); + } + + 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); + } + + 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); + } + + public List ListButtons() + { + Event.WriteVerbose("WebService", "ListButtons"); + + List names = new List(); + for (ushort i = 1; i < WebService.HAC.Buttons.Count; i++) + { + clsButton unit = WebService.HAC.Buttons[i]; + + if (unit.DefaultProperties == false) + names.Add(new NameContract() { id = i, name = unit.Name }); + } + return names; + } + + public void PushButton(CommandContract unit) + { + Event.WriteVerbose("WebService", "PushButton: " + unit.id); + WebService.HAC.SendCommand(enuUnitCommand.Button, 0, unit.id); + } + } +} \ No newline at end of file diff --git a/HAILogger/Helper.cs b/HAILogger/Helper.cs new file mode 100644 index 0000000..5da7f7f --- /dev/null +++ b/HAILogger/Helper.cs @@ -0,0 +1,107 @@ +using HAI_Shared; +using System; +using System.IO; +using System.Runtime.Serialization.Json; +using System.Text; + +namespace HAILogger +{ + static class Helper + { + public static AreaContract ConvertArea(ushort id, clsArea area) + { + AreaContract ret = new AreaContract(); + + ret.id = id; + ret.name = area.Name; + ret.burglary = area.AreaBurglaryAlarmText; + ret.co = area.AreaGasAlarmText; + ret.fire = area.AreaFireAlarmText; + ret.water = area.AreaWaterAlarmText; + + string mode = area.ModeText(); + + if (mode.Contains("OFF")) + ret.mode = "OFF"; + else if (mode.Contains("DAY")) + ret.mode = "DAY"; + else if (mode.Contains("NIGHT")) + ret.mode = "NIGHT"; + else if (mode.Contains("AWAY")) + ret.mode = "AWAY"; + else if (mode.Contains("VACATION")) + ret.mode = "VACATION"; + + return ret; + } + + public static ZoneContract ConvertZone(ushort id, clsZone zone) + { + ZoneContract ret = new ZoneContract(); + + ret.id = id; + ret.name = zone.Name; + ret.status = zone.StatusText(); + + return ret; + } + + public static UnitContract ConvertUnit(ushort id, clsUnit unit) + { + UnitContract ret = new UnitContract(); + + ret.id = id; + ret.name = unit.Name; + + if (unit.Status > 100) + ret.level = (ushort)(unit.Status - 100); + else if (unit.Status == 1) + ret.level = 100; + else + ret.level = 0; + + return ret; + } + + public static ThermostatContract ConvertThermostat(ushort id, clsThermostat unit) + { + ThermostatContract ret = new ThermostatContract(); + + ret.id = id; + ret.name = unit.Name; + + ushort temp, heat, cool, humidity; + + ushort.TryParse(unit.TempText(), out temp); + ushort.TryParse(unit.HeatSetpointText(), out heat); + ushort.TryParse(unit.CoolSetpointText(), out cool); + ushort.TryParse(unit.HumidityText(), out humidity); + + ret.temp = temp; + ret.humidity = humidity; + ret.heatsetpoint = heat; + ret.coolsetpoint = cool; + ret.mode = unit.Mode; + ret.fanmode = unit.FanMode; + + 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/IHAIService.cs b/HAILogger/IHAIService.cs new file mode 100644 index 0000000..8795cf8 --- /dev/null +++ b/HAILogger/IHAIService.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using System.ServiceModel; +using System.ServiceModel.Web; + +namespace HAILogger +{ + [ServiceContract] + public interface IHAIService + { + [OperationContract] + [WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)] + void Subscribe(SubscribeContract contract); + + [OperationContract] + [WebGet(ResponseFormat = WebMessageFormat.Json)] + List ListAreas(); + + [OperationContract] + [WebGet(ResponseFormat = WebMessageFormat.Json)] + AreaContract GetArea(ushort id); + + [OperationContract] + [WebGet(ResponseFormat = WebMessageFormat.Json)] + List ListZonesContact(); + + [OperationContract] + [WebGet(ResponseFormat = WebMessageFormat.Json)] + List ListZonesMotion(); + + [OperationContract] + [WebGet(ResponseFormat = WebMessageFormat.Json)] + List ListZonesWater(); + + [OperationContract] + [WebGet(ResponseFormat = WebMessageFormat.Json)] + List ListZonesSmoke(); + + [OperationContract] + [WebGet(ResponseFormat = WebMessageFormat.Json)] + List ListZonesCO(); + + [OperationContract] + [WebGet(ResponseFormat = WebMessageFormat.Json)] + ZoneContract GetZone(ushort id); + + [OperationContract] + [WebGet(ResponseFormat = WebMessageFormat.Json)] + List ListUnits(); + + [OperationContract] + [WebGet(ResponseFormat = WebMessageFormat.Json)] + UnitContract GetUnit(ushort id); + + [OperationContract] + [WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)] + void SetUnit(CommandContract unit); + + [OperationContract] + [WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)] + void SetUnitKeypadPress(CommandContract unit); + + [OperationContract] + [WebGet(ResponseFormat = WebMessageFormat.Json)] + List ListThermostats(); + + [OperationContract] + [WebGet(ResponseFormat = WebMessageFormat.Json)] + ThermostatContract GetThermostat(ushort id); + + [OperationContract] + [WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)] + void SetThermostatCoolSetpoint(CommandContract unit); + + [OperationContract] + [WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)] + void SetThermostatHeatSetpoint(CommandContract unit); + + [OperationContract] + [WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)] + void SetThermostatMode(CommandContract unit); + + [OperationContract] + [WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)] + void SetThermostatFanMode(CommandContract unit); + + [OperationContract] + [WebGet(ResponseFormat = WebMessageFormat.Json)] + List ListButtons(); + + [OperationContract] + [WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)] + void PushButton(CommandContract unit); + } +} diff --git a/HAILogger/NameContract.cs b/HAILogger/NameContract.cs new file mode 100644 index 0000000..654f0f0 --- /dev/null +++ b/HAILogger/NameContract.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; + +namespace HAILogger +{ + [DataContract] + public class NameContract + { + [DataMember] + public ushort id { get; set; } + + [DataMember] + public string name { get; set; } + } +} diff --git a/HAILogger/Program.cs b/HAILogger/Program.cs new file mode 100644 index 0000000..b9fbd6f --- /dev/null +++ b/HAILogger/Program.cs @@ -0,0 +1,54 @@ +using System; +using System.Diagnostics; +using System.ServiceProcess; + +namespace HAILogger +{ + class Program + { + static CoreServer server; + + static void Main(string[] args) + { + for (int i = 0; i < args.Length; i++) + { + switch (args[i]) + { + case "-c": + Global.dir_config = args[++i]; + break; + } + } + + if (Environment.UserInteractive) + { + Console.TreatControlCAsInput = false; + Console.CancelKeyPress += new ConsoleCancelEventHandler(myHandler); + + Trace.Listeners.Add(new TextWriterTraceListener(Console.Out)); + + server = new CoreServer(); + } + else + { + ServiceBase[] ServicesToRun; + + // More than one user Service may run within the same process. To add + // another service to this process, change the following line to + // create a second service object. For example, + // + // ServicesToRun = new ServiceBase[] {new Service1(), new MySecondUserService()}; + // + ServicesToRun = new ServiceBase[] { new Service() }; + + ServiceBase.Run(ServicesToRun); + } + } + + protected static void myHandler(object sender, ConsoleCancelEventArgs args) + { + server.Shutdown(); + args.Cancel = true; + } + } +} diff --git a/HAILogger/ProjectInstaller.Designer.cs b/HAILogger/ProjectInstaller.Designer.cs new file mode 100644 index 0000000..3d05edb --- /dev/null +++ b/HAILogger/ProjectInstaller.Designer.cs @@ -0,0 +1,56 @@ +namespace HAILogger +{ + partial class ProjectInstaller + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.serviceProcessInstaller = new System.ServiceProcess.ServiceProcessInstaller(); + this.serviceInstaller = new System.ServiceProcess.ServiceInstaller(); + // + // serviceProcessInstaller + // + this.serviceProcessInstaller.Password = null; + this.serviceProcessInstaller.Username = null; + // + // serviceInstaller + // + this.serviceInstaller.ServiceName = "HAILogger"; + // + // ProjectInstaller + // + this.Installers.AddRange(new System.Configuration.Install.Installer[] { + this.serviceProcessInstaller, + this.serviceInstaller}); + + } + + #endregion + + private System.ServiceProcess.ServiceProcessInstaller serviceProcessInstaller; + private System.ServiceProcess.ServiceInstaller serviceInstaller; + } +} \ No newline at end of file diff --git a/HAILogger/ProjectInstaller.cs b/HAILogger/ProjectInstaller.cs new file mode 100644 index 0000000..e950e2b --- /dev/null +++ b/HAILogger/ProjectInstaller.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Configuration.Install; + +namespace HAILogger +{ + [RunInstaller(true)] + public partial class ProjectInstaller : System.Configuration.Install.Installer + { + public ProjectInstaller() + { + InitializeComponent(); + } + } +} diff --git a/HAILogger/ProjectInstaller.resx b/HAILogger/ProjectInstaller.resx new file mode 100644 index 0000000..119855b --- /dev/null +++ b/HAILogger/ProjectInstaller.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + + 196, 17 + + + False + + \ No newline at end of file diff --git a/HAILogger/Properties/AssemblyInfo.cs b/HAILogger/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..fb4e7c2 --- /dev/null +++ b/HAILogger/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +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: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Excalibur Partners, LLC")] +[assembly: AssemblyProduct("HAILogger")] +[assembly: AssemblyCopyright("Copyright © Excalibur Partners, LLC 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("469a23b2-7f66-4a98-a3a0-fb539ae08bf8")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// 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.5.0")] +[assembly: AssemblyFileVersion("1.0.5.0")] diff --git a/HAILogger/Prowl.cs b/HAILogger/Prowl.cs new file mode 100644 index 0000000..8fb1b5e --- /dev/null +++ b/HAILogger/Prowl.cs @@ -0,0 +1,50 @@ +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/Service.Designer.cs b/HAILogger/Service.Designer.cs new file mode 100644 index 0000000..ceaf929 --- /dev/null +++ b/HAILogger/Service.Designer.cs @@ -0,0 +1,42 @@ +namespace HAILogger +{ + partial class Service + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + // + // Service + // + this.AutoLog = false; + this.CanShutdown = true; + this.ServiceName = "HAILogger"; + + } + + #endregion + } +} diff --git a/HAILogger/Service.cs b/HAILogger/Service.cs new file mode 100644 index 0000000..2db5a77 --- /dev/null +++ b/HAILogger/Service.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Diagnostics; +using System.ServiceProcess; +using System.Text; + +namespace HAILogger +{ + partial class Service : ServiceBase + { + static CoreServer server; + + public Service() + { + InitializeComponent(); + } + + protected override void OnStart(string[] args) + { + server = new CoreServer(); + } + + protected override void OnStop() + { + server.Shutdown(); + } + + protected override void OnShutdown() + { + server.Shutdown(); + } + } +} diff --git a/HAILogger/Service.resx b/HAILogger/Service.resx new file mode 100644 index 0000000..73afb87 --- /dev/null +++ b/HAILogger/Service.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + False + + \ No newline at end of file diff --git a/HAILogger/Settings.cs b/HAILogger/Settings.cs new file mode 100644 index 0000000..1083ac4 --- /dev/null +++ b/HAILogger/Settings.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Specialized; +using System.IO; +using System.Net; +using System.Net.Mail; + +namespace HAILogger +{ + static class Settings + { + public static void LoadSettings() + { + NameValueCollection settings = LoadCollection(Global.dir_config + "\\HAILogger.ini"); + + // 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"); + + // mySQL Database + Global.mysql_logging = ValidateYesNo(settings, "mysql_logging"); + Global.mysql_connection = settings["mysql_connection"]; + + // 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 + Global.verbose_unhandled = ValidateYesNo(settings, "verbose_unhandled"); + Global.verbose_event = ValidateYesNo(settings, "verbose_event"); + Global.verbose_area = ValidateYesNo(settings, "verbose_area"); + Global.verbose_zone = ValidateYesNo(settings, "verbose_zone"); + Global.verbose_thermostat_timer = ValidateYesNo(settings, "verbose_thermostat_timer"); + Global.verbose_thermostat = ValidateYesNo(settings, "verbose_thermostat"); + Global.verbose_unit = ValidateYesNo(settings, "verbose_unit"); + Global.verbose_message = ValidateYesNo(settings, "verbose_message"); + } + + private static int ValidateInt(NameValueCollection settings, string section) + { + try + { + return Int32.Parse(settings[section]); + } + catch + { + Event.WriteError("Settings", "Invalid integer specified for " + section); + Environment.Exit(1); + } + + throw new Exception("ValidateInt shouldn't reach here"); + } + + private static int ValidatePort(NameValueCollection settings, string section) + { + try + { + int port = Int32.Parse(settings[section]); + + if (port < 1 || port > 65534) + throw new Exception(); + + return port; + } + catch + { + Event.WriteError("Settings", "Invalid port specified for " + section); + Environment.Exit(1); + } + + throw new Exception("ValidatePort shouldn't reach here"); + } + + private static bool ValidateBool(NameValueCollection settings, string section) + { + try + { + return Boolean.Parse(settings[section]); + } + catch + { + Event.WriteError("Settings", "Invalid bool specified for " + section); + Environment.Exit(1); + } + + throw new Exception("ValidateBool shouldn't reach here"); + } + + private static IPAddress ValidateIP(NameValueCollection settings, string section) + { + if (settings[section] == "*") + return IPAddress.Any; + + if (settings[section] == "") + return IPAddress.None; + + try + { + return IPAddress.Parse(section); + } + catch + { + Event.WriteError("Settings", "Invalid IP specified for " + section); + Environment.Exit(1); + } + + throw new Exception("ValidateIP shouldn't reach here"); + } + + private static string ValidateDirectory(NameValueCollection settings, string section) + { + try + { + if (!Directory.Exists(settings[section])) + Directory.CreateDirectory(settings[section]); + + return settings[section]; + } + catch + { + Event.WriteError("Settings", "Invalid directory specified for " + section); + Environment.Exit(1); + } + + throw new Exception("ValidateDirectory shouldn't reach here"); + } + + private static MailAddress ValidateMailFrom(NameValueCollection settings, string section) + { + try + { + return new MailAddress(settings[section]); + } + catch + { + Event.WriteError("Settings", "Invalid email specified for " + section); + Environment.Exit(1); + } + + throw new Exception("ValidateMailFrom shouldn't reach here"); + } + + private static MailAddress[] ValidateMailTo(NameValueCollection settings, string section) + { + try + { + if(settings[section] == null) + return new MailAddress[] {}; + + string[] emails = settings[section].Split(','); + MailAddress[] addresses = new MailAddress[emails.Length]; + + for(int i=0; i < emails.Length; i++) + addresses[i] = new MailAddress(emails[i]); + + return addresses; + } + catch + { + Event.WriteError("Settings", "Invalid email specified for " + section); + Environment.Exit(1); + } + + throw new Exception("ValidateMailTo shouldn't reach here"); + } + + private static string[] ValidateMultipleStrings(NameValueCollection settings, string section) + { + try + { + if (settings[section] == null) + return new string[] { }; + + return settings[section].Split(','); + } + catch + { + Event.WriteError("Settings", "Invalid string specified for " + section); + Environment.Exit(1); + } + + throw new Exception("ValidateMultipleStrings shouldn't reach here"); + } + + private static bool ValidateYesNo (NameValueCollection settings, string section) + { + if (settings[section] == null) + return false; + if (string.Compare(settings[section], "yes", true) == 0) + return true; + else if (string.Compare(settings[section], "no", true) == 0) + return false; + else + { + Event.WriteError("Settings", "Invalid yes/no specified for " + section); + Environment.Exit(1); + } + + throw new Exception("ValidateYesNo shouldn't reach here"); + } + + private static NameValueCollection LoadCollection(string sFile) + { + NameValueCollection settings = new NameValueCollection(); + + try + { + FileStream fs = new FileStream(sFile, FileMode.Open, FileAccess.Read); + StreamReader sr = new StreamReader(fs); + + while (true) + { + string line = sr.ReadLine(); + + if (line == null) + break; + + if (line.StartsWith("#")) + continue; + + string[] split = line.Split('='); + + for (int i = 0; i < split.Length; i++) + split[i] = split[i].Trim(); + + if (split.Length == 2) + settings.Add(split[0], split[1]); + } + + sr.Close(); + fs.Close(); + } + catch (FileNotFoundException) + { + Event.WriteError("Settings", "Unable to parse settings file " + sFile); + Environment.Exit(1); + } + + return settings; + } + } +} diff --git a/HAILogger/SubscribeContract.cs b/HAILogger/SubscribeContract.cs new file mode 100644 index 0000000..2448246 --- /dev/null +++ b/HAILogger/SubscribeContract.cs @@ -0,0 +1,11 @@ +using System.Runtime.Serialization; + +namespace HAILogger +{ + [DataContract] + public class SubscribeContract + { + [DataMember] + public string callback { get; set; } + } +} diff --git a/HAILogger/ThermostatContract.cs b/HAILogger/ThermostatContract.cs new file mode 100644 index 0000000..d96fad7 --- /dev/null +++ b/HAILogger/ThermostatContract.cs @@ -0,0 +1,33 @@ +using HAI_Shared; +using System.Runtime.Serialization; + +namespace HAILogger +{ + [DataContract] + public class ThermostatContract + { + [DataMember] + public ushort id { get; set; } + + [DataMember] + public string name { get; set; } + + [DataMember] + public ushort temp { get; set; } + + [DataMember] + public ushort humidity { get; set; } + + [DataMember] + public ushort coolsetpoint { get; set; } + + [DataMember] + public ushort heatsetpoint { get; set; } + + [DataMember] + public enuThermostatMode mode { get; set; } + + [DataMember] + public enuThermostatFanMode fanmode { get; set; } + } +} diff --git a/HAILogger/UnitContract.cs b/HAILogger/UnitContract.cs new file mode 100644 index 0000000..69563a6 --- /dev/null +++ b/HAILogger/UnitContract.cs @@ -0,0 +1,17 @@ +using System.Runtime.Serialization; + +namespace HAILogger +{ + [DataContract] + public class UnitContract + { + [DataMember] + public ushort id { get; set; } + + [DataMember] + public string name { get; set; } + + [DataMember] + public ushort level { get; set; } + } +} diff --git a/HAILogger/WebNotification.cs b/HAILogger/WebNotification.cs new file mode 100644 index 0000000..c0d4b92 --- /dev/null +++ b/HAILogger/WebNotification.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Net; + +namespace HAILogger +{ + static class WebNotification + { + private static List subscriptions = new List(); + + public static void AddSubscription(string callback) + { + if (!subscriptions.Contains(callback)) + { + Event.WriteVerbose("WebRequest", "Adding subscription to " + callback); + subscriptions.Add(callback); + } + } + + public static void Send(string type, string body) + { + WebClient client = new WebClient(); + client.Headers.Add(HttpRequestHeader.ContentType, "application/json"); + client.Headers.Add("type", type); + client.UploadStringCompleted += client_UploadStringCompleted; + + foreach (string subscription in subscriptions) + { + 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); + subscriptions.Remove(e.UserState.ToString()); + } + } + } +} diff --git a/HAILogger/WebService.cs b/HAILogger/WebService.cs new file mode 100644 index 0000000..f879cab --- /dev/null +++ b/HAILogger/WebService.cs @@ -0,0 +1,44 @@ +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/HAILogger/ZoneContract.cs b/HAILogger/ZoneContract.cs new file mode 100644 index 0000000..3c0d28d --- /dev/null +++ b/HAILogger/ZoneContract.cs @@ -0,0 +1,17 @@ +using System.Runtime.Serialization; + +namespace HAILogger +{ + [DataContract] + public class ZoneContract + { + [DataMember] + public ushort id { get; set; } + + [DataMember] + public string name { get; set; } + + [DataMember] + public string status { get; set; } + } +} diff --git a/README.md b/README.md index f14fe51..84a86a9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,87 @@ # HAILogger Provides logging and web service API for HAI/Leviton OmniPro II controllers + +##Download +You can download the [binary here](http://www.excalibur-partners.com/downloads/HAILogger_1_0_5.zip) + +##Requirements +- .NET Framework 4.0 +- mySQL 5.1 ODBC Connector + +##Operation +- Area, Messages, Units, and Zones are logged to mySQL when status changes +- Thermostats are logged to mySQL once per minute + - If no notifications are received within 4 minutes a request is issued + - After 5 minutes of no updates a warning will be logged and mySQL will not be updated + - If the temp is 0 a warning will be logged and mySQL will not be updated +- Controller time is checked and compared to the local computer time disregarding time zones + +##Notifications +- Emails are sent to mail_alarm_to when an area status changes +- Prowl notifications are sent when an areas status changes + +##Installation +1. Copy files to your desiered location like C:\HAILogger +2. Create mySQL database and import HAILogger.sql +3. Update HAILogger.ini with settings +4. Run HAILogger.exe and verify everything is working +5. For Windows Service run install.bat / uninstall.bat + +##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. + +http://dev.mysql.com/downloads/mysql/ +http://dev.mysql.com/downloads/tools/workbench/ +http://dev.mysql.com/downloads/connector/odbc/5.1.html + +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. + +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. +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. + +##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 +Version 1.0.5 - 2016-11-15 +- Added web service API for Samsung SmartThings integration + +Version 1.0.4 - 2014-05-08 +- Merged HAILogger.exe and HAILoggerService.exe +- Added immediate time sync after controller reconnect + +Version 1.0.3 - 2013-01-06 +- Added setting for prowl console message notification +- Added settings for verbose output control +- Added setting to enable mySQL logging +- Added queue to mySQL logging +- Changed mySQL log time from mySQL NOW() to computer time +- Changed prowl notifications to be asynchronous +- Fixed crash when prowl api down +- Fixed setting yes/no parsing so no setting works +- Fixed incorrect thermostat out of date status warning + +Version 1.0.2 - 2012-12-30 +- Fixed thermostat invalid mySQL logging error + +Version 1.0.1 - 2012-12-30 +- Added setting to adjust time sync interval +- Fixed crash when controller time not initially set + +Version 1.0.0 - 2012-12-29 +- Initial release \ No newline at end of file